Skip to content

Fix/workspace share resolution - #152

Open
hmahmood24 wants to merge 5 commits into
stagingfrom
fix/workspace-share-resolution
Open

Fix/workspace share resolution#152
hmahmood24 wants to merge 5 commits into
stagingfrom
fix/workspace-share-resolution

Conversation

@hmahmood24

Copy link
Copy Markdown
Member

Summary

Type of change

  • Bug fix (non-breaking change that fixes incorrect behavior)
  • Feature (non-breaking change that adds functionality)
  • Refactor (no behavior change)
  • Breaking change (API or data-model change — Unify has zero-backward-compat policy, but please call it out)
  • Test-only (no source changes)
  • Docs / chore / CI

Areas touched

  • Actor / CodeAct
  • ConversationManager / slow brain
  • A specific state manager (Contact / Knowledge / Task / Transcript / Guidance / Function / File / Image / Web / Secret / Blacklist / Data / Memory)
  • Async tool loop (unify/common/_async_tool/)
  • Event bus / observability
  • Gateway / external comms
  • Tests / test infra (tests/, conftest.py, parallel_run.sh)
  • CI / build / packaging

Test plan

tests/parallel_run.sh tests/...
  • All relevant tests pass locally
  • If this is a bug fix, I added a regression test (or explained why one isn't feasible)

Behavior / migration notes

None.

Checklist

  • PR is targeted at staging (not main)
  • Followed conventional commit style (feat(scope):, fix(scope):, refactor(scope):, chore(scope):, etc.)
  • No try/except added defensively — only around specific, recoverable errors
  • No "new" / "updated" / "TODO from chat" temporal comments (see .agents/rules/no-temporal-comments.md)
  • No test-specific shortcuts in production code (see .agents/rules/no-test-info-in-production-code.md)
  • Updated AGENTS.md / ARCHITECTURE.md if I changed architectural conventions

A provider sharing link resolves through /shares/{token}/driveItem, and
the proxy broke that route four separate ways. Share tails were fed to
the drive-tail classifier, but a share's driveItem is an item, not a
drive root -- so /content (the canonical shared-file download) and the
colon path forms classified unknown and 403'd, while /children
allow-checks built /driveItem/root, a segment beneath an item that Graph
rejects, turning every share listing into an upstream 400. Share tails
now classify as item subresources anchored on the driveItem, and the
ancestry layer maps a share base's root anchor onto the driveItem-
relative URL shapes.

Redirects were dead ends. Graph answers share-token requests with a 308
the sandbox cannot follow -- it holds the proxy nonce, not a provider
token -- and the ancestry clients did not follow redirects either. Share
reads now follow redirects inside the proxy, where the real token lives
(httpx drops it on cross-origin hops by itself); a Location that still
reaches the sandbox is rewritten to the proxy root, where the nonce
works; and a redirect the client cannot follow surfaces as a 502 naming
the failure instead of an opaque proxy 500. This is the observed staging
failure: an authenticated /shares request died as a bare 308 with
nothing anyone could act on.

Nothing redeemed the link. An anyone-with-the-link or cross-tenant share
is unusable through /shares until the recipient redeems it -- the API
equivalent of opening it in a browser -- and such shares never appear in
sharedWithMe, so the item is invisible everywhere until then. The proxy
now sends Prefer: redeemSharingLinkIfNecessary on shares requests; the
caller handed the assistant the link, so redemption is the intent, and
IfNecessary keeps it a no-op where access already exists. A caller-
supplied Prefer wins.
…orkspace

The proxy could not serve the shares surface, and nothing told the actor
it existed -- so faced with a SharePoint sharing URL it scraped the share
page's HTML and extracted page-embedded grants, exactly the fragile,
credential-leaking route the workspace connection exists to replace.

Three additions, one per layer. The prompt's OAuth section gains the
recipe: encode the URL into a u! share token, walk /shares/{token}/
driveItem through the proxy, or re-address via /drives/{driveId}/items/
{id} after the first resolution -- never fetch the link itself. A
platform/workspace-sharing-links builtin guidance entry carries the same
procedure with the failure modes (absence from sharedWithMe says nothing
about access; older runtimes need the Prefer header set by hand) to
every assistant through the hash-guarded platform catalogue. And
get_oauth_prompt_context now names which providers actually hold a live
connection, read from the runtime secret store without a network call --
plans stop guessing whether the workspace works and stop asking the user
to reconnect accounts that already do.
A provider app whose sync succeeded with zero rows reported plain
"ready", which reads as usable and sends the actor searching for tools
that do not exist. The zero-tool case now names the condition and the
remedy -- likely missing credentials or scopes, reconnect the app --
so the prompt line and Console stop telling different stories.
… the run

Three defects in the two-phase submit, each found by walking the bulk
path end to end rather than by a test failing.

Source files were read wholly into memory to be uploaded -- a 346 MB CSV
cost 346 MB of pod RAM inside the very boundary the dispatch tier exists
to protect, and it is exactly the multi-hundred-MB files that dispatch.
Uploads now stream from disk.

The stage-upload-publish sequence ran synchronously inside submit(),
holding the caller's thread for as long as the uplink takes while the
prompt promises a handle immediately. Dispatch now runs on the manager's
pool, like inline execution, and its failures land on the run row like
every other outcome.

A dispatch that raised -- or a pod that died -- between the run-row
insert and the publish left the run queued forever, the one state whose
next step is "keep polling", for a run no worker will ever pick up. The
raise now records a failure; the pod-death case is settled at read time,
deterministically: uploads only ever run in the submitting process, so a
dispatched row with no dispatch id and no upload in flight in this
process has no future. No timers, no grace periods. A request whose
source stages no files is refused before the publish, because a zero-job
dispatch succeeds at every step and then folds to queued eternally.
…xecute them

Dispatching a RowsSource or TableSource above MAX_INLINE_ROWS published
zero jobs and hung forever. The fleet's unit of work is a staged file --
publish_submit emits one parse message per uploaded source, and a rows
or table request stages none -- so the dispatch wrote a manifest with an
empty job list and its status folded to queued eternally, unreachable by
every retry scope. This is precisely the bulk case the tier existed for:
any connected-app pull over ten thousand rows, with a fleet reachable.
It was never seen live only because the fleet was invisible to
assistants until the control-plane env fix landed.

choose_tier now keeps rows and tables in process whatever their size.
The inline engine checkpoints, verifies against the declared count and
resumes, so size costs the assistant latency and contention rather than
correctness -- against an eternal hang there is no trade to weigh. This
consciously amends the standing decision that rows dispatch above a
measured ceiling: that decision assumed a rows job type the fleet does
not have. MAX_INLINE_ROWS stays as the documented boundary such a job
type will restore, and the previous commit's pathless-dispatch refusal
backstops any routing regression. The ingestion special_note follows.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant