Repository navigation
CL-7107: re-pin Interchange vendor to 692c3106 - #632
TheGreatAxios merged 64 commits into
Conversation
5e636d2 to
96443c0
Compare
bdcc2cd to
f309c48
Compare
Self-reviewRebased onto origin/main (resolving the tsBuildInfoFile/kill-dates.txt conflicts from #629) and reviewed all 18 non-vendor files plus the VENDORED.md ledger; one stale test assertion is fixed, the rest of the review turned up no defects, but typecheck and several build-test failures pre-date this review pass and remain open.
Targeted tests: |
a9d1048 to
70d9865
Compare
Status 2026-09-04Owner ruling: go fully native. The stranded adopting-deploy front is not re-authored; chat agents move onto Interchange's provisioned deployments instead, tracked in CL-6534 (Retire folded runs). This PR stays open and lands together with that work, at which point the remaining type errors close and the sidecar identity validator is wired from Already on this branch and green in |
) Step 1 of CL-6534. ## Why Workbench is going fully native on Interchange: the UI becomes an Interchange client and the backend becomes mostly stock Interchange, with custom backend machinery removed. Two constraints shape this step: - It must stay **easy for an operator to decide what kinds of sidecars to rig up and how to structure them.** - The **default install, with no provisioner configured, must still work on a single server** — many chats and workflows on the same VPS. Today an unconfigured hub registers *no* sidecar provisioner, so exclusive sidecar placement fails closed until an operator sets up Docker or an E2B account. That is the wrong default for a single-server install, and it is also the shape the later steps (retiring folded runs) need in place first: every run becomes an allocation, and an allocation needs a backend that exists out of the box. ## What this does **Adds `packages/process-provisioner`** — a `SidecarProvisioner` whose `ensure` spawns one `apps/sidecar` process per allocation on the hub's own host, mirroring the env `bun run dev` gives the local sidecar: the hub WebSocket URL, the allocation's provisioner-issued bearer token, its sidecar id, and a per-allocation data directory under `HUB_DATA_DIR`. `destroy` sends SIGTERM (escalating to SIGKILL after a grace period) and removes that directory. It supplies only the OS-level unit. Idempotence, generation fencing, destroy tombstones, and obsolete-unit sweeping come from `@corbits/sandbox-sidecar`'s shared core — the same core `docker` and `e2b` use. A unit is scoped by generation on disk (`allocations/<id>/gen-<n>/sidecar.pid`), which is what keeps the sweep honest across a hub restart: a new generation's pid never overwrites the record of the unit it replaces, so a superseded process is still found and stopped. Binding fingerprint is `process:v1:<sidecar entry>:<hub ws url>`. Every OS failure path reports through `reportError` with the allocation id and returns a classified rejection carrying the `refId`. **Registers it as the default.** With `SIDECAR_PROVISIONERS` unset the hub registers `process` as its sole backend and default. With it set, behaviour is exactly as before — explicit ids, the `SIDECAR_DEFAULT_PROVISIONER` rules, loud boot failures for an unknown/duplicate id or a missing backend setting — with `process` accepted as an id like any other. **Wires the sidecar identity validator.** `createSidecarRouter`'s `validateSidecarIdentity` was never supplied, so it fell back to the router's always-true default. It is now the same `createSidecarCredentialResolver({ db })` that authenticates the handshake: `resolve` for the token, `isCurrent` for the registration, readiness, and routing boundaries — exactly how upstream's own `apps/hub/src/server.ts` wires it. Without it a provisioner-issued token stays accepted after its allocation was superseded or destroyed, which a process-provisioned sidecar reaches easily: a child that outlives its allocation keeps reconnecting to the same hub. ## How to configure ```sh # Nothing set: `process` alone, on this host. This is the default. # Or name backends explicitly (this replaces the default; list `process` # too if you want it alongside another): SIDECAR_PROVISIONERS=process,docker SIDECAR_DEFAULT_PROVISIONER=docker DOCKER_PROVISIONER_IMAGE=ghcr.io/... # Optional overrides for the process backend — unset, it spawns this # repository's apps/sidecar/src/index.ts with the bun running the hub: PROCESS_PROVISIONER_SIDECAR_ENTRY=/opt/workbench/apps/sidecar/src/index.ts PROCESS_PROVISIONER_RUNTIME=/usr/local/bin/bun ``` `docs/sidecar-provisioners.md` is the one page an operator reads: the three shipped backends side by side (isolation, requirements, where each keeps its hub-side state) and the three steps to add another. `.env.example` carries the same set against `SIDECAR_PROVISIONERS`, and IMPLEMENTATION.md's deployment section points at the doc. ## Verification - `packages/process-provisioner` unit suite: spawn injected, so no real process starts. Asserts the spawned env, idempotent ensure, stale generation rejection, a newer generation superseding and stopping the old process, spawn failure classification, destroy cleanup and tombstone fencing, and allocation-id validation. - `apps/hub/test/config.test.ts` covers the new default, `process` as an explicit id, its overrides, and that an explicit list without `process` does not get it. - `bun run typecheck`, `bun run lint`, `bun run check:structural`: green. - DB-gated suites run locally against Postgres (`apps/hub/test/composition.test.ts`, `packages/sidecar-placement`): green — the hub boots with the new default backend and the identity validator wired. - No end-to-end harness boots the hub with a provisioner and dials a real sidecar back, so the live check is the browser walkthrough: `bun run dev`, then `bun run scripts/e2e/browser/walkthrough.ts`. ## Notes Upstream's `SidecarProvisioner` at the #632 target pin (692c3106) adds a `capabilities: readonly SidecarCapabilityDeclaration[]` member and an optional-`externalRef` clarification that main's vendored contract does not have. This provisioner is built against what main has; declaring capabilities lands with the #632 re-pin, alongside the same addition for the docker and e2b backends. Nothing is shimmed for it here. Part of CL-6534
8a712fe to
181c143
Compare
catalogWorkflowDeployableOnThisPin (@corbits/seeding) derives, from each CATALOG_WORKFLOWS entry's own rendered definition, whether it carries credentialBindings the current Interchange pin's deploy front has no credentialCipher seam to resolve. The template-blocks deploy route now calls it before invoking deployWorkflowSource and answers a 409 not_deployable_yet envelope with consumer-safe copy for the six credential-bound entries, instead of letting the vendored deployer throw into the route's generic 500 branch. Per the CL-7073 critique ruling: this does not route through the adopting deploy front or add a credential-cipher seam to the vendored deployer — that seam lands with the Interchange re-pin (CL-7107 / PR #632, pin 692c3106). Until then this is the honest gap to state, not paper over.
…7073) Records why the six credential-bound CATALOG_WORKFLOWS entries 409 instead of deploying on this pin, and that they become addable at the Interchange re-pin (CL-7107 / PR #632, pin 692c3106).
catalogWorkflowDeployableOnThisPin (@corbits/seeding) derives, from each CATALOG_WORKFLOWS entry's own rendered definition, whether it carries credentialBindings the current Interchange pin's deploy front has no credentialCipher seam to resolve. The template-blocks deploy route now calls it before invoking deployWorkflowSource and answers a 409 not_deployable_yet envelope with consumer-safe copy for the six credential-bound entries, instead of letting the vendored deployer throw into the route's generic 500 branch. Per the CL-7073 critique ruling: this does not route through the adopting deploy front or add a credential-cipher seam to the vendored deployer — that seam lands with the Interchange re-pin (CL-7107 / PR #632, pin 692c3106). Until then this is the honest gap to state, not paper over.
…7073) Records why the six credential-bound CATALOG_WORKFLOWS entries 409 instead of deploying on this pin, and that they become addable at the Interchange re-pin (CL-7107 / PR #632, pin 692c3106).
Hand-ported the storage-only pieces of RFC 5322 Message-ID threading from the entangled 3a60464 / 5bec1a7 commits on cl-7107-repin-and-re-vendor-audit-against-interchange-main-before-the: the workbench_messages.mail_message_id column and its migration (0029), stampMailMessageId/findByMailMessageId on both the drizzle and in-memory room-message stores, mail-headers.ts (mailMessageIdFor, rowIdFromMailMessageId, mailThreadHeaders, parentMailMessageId, parseReferences) with its unit tests, and mailAncestryOf in threads.ts. Left behind: the vendored-pin changes on that branch, and every dispatch-path change depending on the new pin (chat-orchestrator, turn-queue, folded-runs mail, interaction-tools) — those are out of scope here and land with their own ticket.
Hand-ported the storage-only pieces of RFC 5322 Message-ID threading from the entangled 3a60464 / 5bec1a7 commits on cl-7107-repin-and-re-vendor-audit-against-interchange-main-before-the: the workbench_messages.mail_message_id column and its migration (0029), stampMailMessageId/findByMailMessageId on both the drizzle and in-memory room-message stores, mail-headers.ts (mailMessageIdFor, rowIdFromMailMessageId, mailThreadHeaders, parentMailMessageId, parseReferences) with its unit tests, and mailAncestryOf in threads.ts. Left behind: the vendored-pin changes on that branch, and every dispatch-path change depending on the new pin (chat-orchestrator, turn-queue, folded-runs mail, interaction-tools) — those are out of scope here and land with their own ticket.
Hand-ported the storage-only pieces of RFC 5322 Message-ID threading from the entangled 3a60464 / 5bec1a7 commits on cl-7107-repin-and-re-vendor-audit-against-interchange-main-before-the: the workbench_messages.mail_message_id column and its migration (0029), stampMailMessageId/findByMailMessageId on both the drizzle and in-memory room-message stores, mail-headers.ts (mailMessageIdFor, rowIdFromMailMessageId, mailThreadHeaders, parentMailMessageId, parseReferences) with its unit tests, and mailAncestryOf in threads.ts. Left behind: the vendored-pin changes on that branch, and every dispatch-path change depending on the new pin (chat-orchestrator, turn-queue, folded-runs mail, interaction-tools) — those are out of scope here and land with their own ticket.
Fixes CL-7450 Part of CL-7445 ## Summary - Ports the storage-only slice of RFC 5322 Message-ID threading from PR #632's branch (`cl-7107-repin-and-re-vendor-audit-against-interchange-main-before-the`, commits `3a604647` and `5bec1a73`): the `workbench_messages.mail_message_id` column and its migration (0029), `stampMailMessageId`/`findByMailMessageId` on both the drizzle and in-memory room-message stores, `packages/chat/src/mail-headers.ts` (+ its unit tests), and `mailAncestryOf` in `threads.ts`. - `sendWorkbenchMessage` runs a strict order: store the row -> stamp its own Message-ID -> fan it into every human participant's `@corbits/mailbox` inbox as ONE batch (one `@corbits/mailbox` transaction, via `writeMailboxMessages`) -> publish the row onto the room's live stream -> dispatch to whichever agents it names. A fan-out failure deletes the just-inserted (never-published) row and fails the send outright — no phantom bubble, no duplicate row on a client retry. - Every copy — an "outbound" row in the sender's own mailbox, "inbound" rows in every other human's — carries the row's own Message-ID as its frame header and cached column; idempotency rides `@corbits/mailbox`'s own default transport key, never a caller-minted one. A reply carries `In-Reply-To`/`References` from the thread chain. - A shared (projected-tenant) workbench's rows live in the OWNING tenant: the Message-ID/`In-Reply-To` and every recipient's address are minted against the owning tenant's own mail domain, never the acting caller's. The acting sender's principal is resolved in the owner tenant; a share member with no principal there has its own outbound copy skipped quietly (debug log, not a report) while every other human still gets theirs. - A bodyless (attachment-only) message gets a real subject (`Attachment: <name>`) and a body listing its parts, instead of `(no subject)`/an empty body. - The write sits behind a small `MailboxWriter` port (`packages/chat/src/mailbox-fanout.ts`) — a thin pass-through onto `@corbits/mailbox`'s own `writeMailboxMessages` batch call, no hand-rolled inserts — so the fan-out logic (who gets a row, which direction, the shared Message-ID and refs) is unit-testable without Postgres. - A participant address with no matching tenant principal is reported and skipped; any other batch failure is reported and rethrown, never swallowed. - `apps/hub/src/index.ts` wires the new `mailbox` dep (writer + `resolveKnownPrincipalIds` + `resolveTenantDomain`) onto the existing `mailboxDb`/`mailboxBus`, kept to the minimum lines needed since another branch (`.worktrees/cl-mailbox-persist-wrap`) is also touching this file's `persistMail` wrapper. - Bumps `@corbits/chat`'s `@corbits/mailbox` pin to `118a2cd3`, matching `@corbits/inbox`'s. ## Test plan - [x] `packages/chat` unit suite (non-UI): 924 pass, 0 fail (`bun test packages/chat/src packages/chat/test`) - [x] DB-gated suite against local Postgres (`DATABASE_URL=postgres://postgres:postgres@localhost:5432/workbench`): all 3 `mailbox-fanout.drizzle.test.ts` cases pass, including the folded critique frame-Message-ID probe and the batch partial-failure/retry probe - [x] `packages/chat` and `apps/hub` (`@workbench/hub`) typecheck clean - [x] `bun run lint` clean (pre-existing unrelated warnings only) - [x] `bun run check:structural` clean (0 new `report-error` findings) - [ ] `gh pr checks --watch --fail-fast` (running next) ## Follow-up fixes (critique findings) - **Finding 1 (HIGH)**: the dispatched agent frame now carries the row's own RFC 5322 Message-ID and In-Reply-To/References, ported from PR #632's dispatch-path commits (`3a604647`) onto this branch's already- landed storage slice — `dispatchTurn` -> `MailContent` (`codec.ts`) -> `WorkbenchMail.sendMail` (`platform-adapter.ts`) -> `SendFoldedMailParams` (`folded-runs/src/mail.ts`) -> the native `UserMessageParams.messageId`/`inReplyTo`/`references` vendor's `hub-sessions` already accepts. - **Finding 2 (MEDIUM)**: the mailbox fan-out now writes `References` (the full ancestry chain, oldest first) alongside `In-Reply-To`. - **Finding 3 (MEDIUM)**: a fan-out failure is now a typed `MailboxFanoutFailedError` carrying its own `refId`, so the send route quotes that ref in a 502 rather than letting the failure fall through to the hub's generic unhandled-route-error handler and get reported a second time. The "nothing has seen it yet" doc/comment claim is corrected: chat and `@corbits/mailbox` are separate Postgres handles in the hub's composition, so this is not transactional and the honest (bounded) window is documented instead of papered over. - **Finding 4 (LOW)**: migration `0029_workbench_messages_mail_message_id` is shared BY NAME with PR #632's branch (`cl-7107-repin-and-re-vendor-audit-against-interchange-main-before-the`) — that branch should rebase onto this one once it merges, not the reverse, to avoid a duplicate/conflicting `0029`.
Re-copy every vendor/intx/* package from faremeter/interchange
origin/main at 692c3106 ("Remove the orphaned sidecar challenge
signer"), 90 commits past the v0.3.0 tag, and re-apply the workbench
deltas the ledger records.
Restores the vendoring convention the tree had before this branch:
upstream test files are not vendored, because they import the
unvendored @intx/inference-testing and @intx/test-harness test
packages. Only the ten tests that cover a workbench delta are kept.
The previous pass copied all 242 upstream test files, which is what
made typecheck fail on those imports.
Deltas retired at this pin:
- workflow-host CL-6448's credential thread is now half native:
upstream passes the run's live credential-material cell into every
body spawn as `credentialMaterial`. The `credentialWiring` field
stays, because it additionally carries the per-step
`resolveStepGrants` a body's tool bundles need and upstream's cell
does not.
Deltas this pin strands, recorded in VENDORED.md rather than
re-authored: the CL-6324 session-service deltas (the adopted deploy
front, the per-run `sourceRef` threading) and CL-7191's
`UserMessageParams.correlationId` hung off upstream's
`deployWorkflowFromSource`, which this pin deletes in favour of the
catalog-offering plus provisioner-selection path. session-service.ts
is vendored pristine.
Workbench db migrations are renumbered to 0089-0092 behind upstream's
new 0087_drop_sidecar_placement and 0088_thick_sprite; the drizzle
journal keeps our original `when` values, which still sort after
upstream's.
The kill-date registry moves in the same commit, as the vendored-tree
hash rule requires.
The re-pinned @intx/hub-agent and @intx/workflow-host expect the host to supply a child grant filter, per-step credential capabilities, and a workflow-run record writer. Derived from upstream's own apps/sidecar at the pin, which is never npm-published.
- Drop `signEd25519` from the orchestrator's `SidecarCryptoOps`: the pin removes the orphaned sidecar challenge signer, so the field no longer exists. The sidecar's own supervisor and identity vault keep calling `@intx/crypto` directly. - Thread the spawn seam's `depth` / `maxChildSpawnDepth` and the native `credentialMaterial` argument through the child runtime. - Attach a `cause` to the SIDECAR_TOOL_REGISTRIES JSON parse failure and stop pre-seeding `dirtyExists`, both of which lint flags.
Move every ledger row and VENDORED-FROM commit line to 692c3106, roll the kill dates, and correct the rows whose deltas changed: the db migration renumbering, workflow-host's now-native credential thread, and hub-sessions' stranded adopted-deploy front. Adds a section recording the two deltas this pin strands and the ruling each needs: the CL-6324 adopted deploy front (upstream deleted the `deployWorkflowFromSource` seam it hung off) and packages/sidecar-placement (upstream replaced the placement model with sidecar capabilities and registration-order provisioner selection).
Interchange unified inference and tool credentials onto one material cell: an `InferenceSource` names a `credentialId` instead of carrying an inline `apiKey`, and the reactor fills the request's secret at send time through `BaseEnv.readCurrentMaterial`. Without that resolver the agent's fail-closed default refuses every inference call. Add `stepInferenceMaterialResolver`, which reads the step's live `CredentialWiring.materialRef` by credential id -- fresh per call, so a `credentials-updated` frame mid-step is picked up, and failing closed when the credential was revoked or never delivered -- and wire it into the per-step env beside the existing tool-credential slot. A step with no credential wiring omits the key and keeps the platform default. Test fixtures move from `apiKey` to `credentialId`. Two comments claiming the on-disk deployment and run records embed source API keys are corrected: a record now names a credentialId, not a secret. The 0o600 mode stays, since the record still carries the run's routing identity.
The multistep credentials router no longer re-validates the delivery it is handed. It cannot be handed a malformed one: hub-link's `handleMessage` narrows every inbound frame through `HubFrame` (which unions `CredentialsUpdateFrame`) and answers a malformed frame with an error frame before any router is consulted. So the test asserts the boundary's contract -- the frame schema rejects the shape -- rather than asking our adapter to defend against a shape that can never reach it.
`SidecarProvisioner` now requires a `capabilities` declaration, which the hub's capability policy matches a deployment's requirements against. Every workbench provisioner declares the empty set, which matches any deployment that states no requirement -- the behaviour each had before Interchange replaced the placement model with capability selection. The field is required rather than defaulted so a backend that grows a real capability has to say so at its construction site.
`InferenceSource` names a `credentialId` at this pin instead of carrying an inline `apiKey`, so a caller-supplied sources override built the old way now fails `parseSourcesOverride`'s arktype narrow.
Interchange handles sidecar placement itself as of upstream 692c3106: the `SidecarPlacementRequirement` model this root-bucket module was built on is gone, replaced by declared sidecar capabilities plus registration-order provisioner selection, and upstream's own migration 0087 drops the table. A per-tenant placement flag can no longer be honored by the platform, so the module and the product surface that wrote it are removed rather than re-pointed at a contract that no longer exists. Removed: `packages/sidecar-placement` (package, its route mount in apps/hub, its workspace dependency and project reference), the Capacity settings section (`capacity-api.ts`, `capacity-section.tsx` and their suites), its nav id, probe state and strings, and the adoption doc. No workbench-owned migration existed for it -- the module read and wrote `TenantConfig.sidecarPlacement` on the tenant row directly.
…s own kill fallback (#653) hub.shutdown() stops the sidecar then the hub in sequence, and each stop() gives its process up to 5s to exit on SIGTERM before escalating to SIGKILL. A graceful-but-slow exit on either process alone can approach Bun's 5000ms default hook timeout, and two of them back to back can exceed it outright, which is what the isolation CI job hit: the hook timed out at ~5001ms with every real assertion passing. Raise the hook's timeout to 20s so a slow-but-clean shutdown on a loaded CI runner has room to finish.
…(CL-7463) (#655) * hub: point stale folded-runs comments at the packages that replaced it Several comments in apps/hub still named the deleted @corbits/folded-runs package and its FoldedRunsDeps type. Point them at createHubChatPlatform's CreateHubChatPlatformDeps, @corbits/workflows, and @corbits/run-scope, which now own the referenced behavior. * web: retire stale folded-run terminology in top-level-run comments Several comments described the scope-routes filter using the deleted @corbits/folded-runs package and a nonexistent folded_run marker table. Reworded them around the actual predicate (top-level vs non-top-level run) and pointed the package references at @corbits/run-scope. * sidecar: point lazy-wake comments at chat's wakeByAddress, not folded-runs The deferred-to-wake restore path's comments still named the deleted @corbits/folded-runs package's wakeFoldedRun/deployAtHead. Point them at @corbits/chat's wakeByAddress, which now backs the lazy-wake port. * agent-directory: drop folded-run terminology from client filter docs purposeAgentInstances' excludeRunIds no longer has a real caller in this repo (apps/web now scopes server-side via @corbits/run-scope), and the README pointed at a deleted @corbits/chat-ui helper. Rewrote the comments, README, and test names around the actual excludeRunIds contract. * chat: point stale folded-runs function names at their real replacements schema.ts and test comments still named the deleted @corbits/folded-runs package (inferenceSourcesDigest, findFoldedRunByAddress, wakeFoldedRun, sendFoldedMailWithReclaimRetry) — none of which exist any more. Point them at the actual in-package names: offeringDigest, the real address→run resolution, wakeByAddress, sendRunMailWithReclaimRetry. * run-key-history: drop reference to a nonexistent folded-runs lookup reconnect.ts claimed the hub wires this alongside @corbits/folded-runs' lookupFoldedRunReconnectKey, which no longer exists anywhere. Also pointed the drizzle test's scope-routes cross-reference at @corbits/run-scope. * misc: point remaining stale folded-runs cross-references at real owners describe.ts, credential-error.ts, the seeding launchability test, collections' README, and the code-review/last-30-days-research workflow comments still named the deleted @corbits/folded-runs package or its files. Pointed each at where the described behavior actually lives now (@corbits/chat, @corbits/workflows, or vendor/intx/hub-sessions). * db-setup: drop the dead folded_runs schema from the reset allowlist No package creates a folded_runs Postgres schema any more (the package was deleted); also renamed the run-scope drizzle test's isolation schema names off the same dead term.
…657) * Add tests for deploying default workflows by catalog offering ids seedTenant currently deploys every default workflow with a bare provider/baseURL/apiKey source, but the hub's deploy route now requires sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's catalog. Update the seeding, onboarding, and hub config test suites to expect the new offering-id based deploy body and provider/model-only ModelSource shape ahead of the implementation change. * Seeding: deploy default workflows by catalog offering ids ensureDeployment posted a bare {provider, baseURL, apiKey, model} source, but the hub's deploy route requires sourceOfferingIds and a defaultSourceOfferingId resolved from the tenant's catalog, so seeding a connect flow's "assistant" workflow 400'd. seedTenant now resolves the tenant's real catalog offerings (sorted by priority, lowest wins the default) for a normal workflow, and plants a dedicated noop catalog offering for a modelSource-pinned one (heartbeat), pointed at the hub's own noop-inference endpoint. ModelSource shrinks to a bare provider/model pair — the baseURL/apiKey it used to carry only mattered for the old deploy body, and callers that still need a key for seeding a catalog (system-seed's root tenant, config's env-key auto-plant) keep it on their own local type instead. * Fix formatting: prettier * Add tests for resolved catalog offerings and modelSourceFor's trimmed signature Covers the new GET .../catalog/resolved-offerings route (denied grant, and both directly-owned and ancestor-inherited offerings sorted by priority) and updates seeding/onboarding test fixtures to the route's response shape ahead of the implementation change. * Seed deploys against a tenant's resolved catalog offerings, not just owned ones resolveRealSourceOfferingIds listed only offerings a tenant owns directly, missing anything inherited from an ancestor tenant — the hub's own workflow deployer already resolves that wider, ancestor-inheriting view via listVisibleOfferings. Adds GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from apps/hub, implemented in @corbits/inference-catalog so the deployer and this route share one source of truth) and points seed at it instead. Also drops the two long-dead unused parameters from onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from @corbits/seeding for the e2e harness to reuse. * Fix remaining modelSourceFor call sites for its trimmed signature play.ts, greeting-delivery.test.ts, and local-rip.test.ts each still called modelSourceFor with the apiKey/baseURLOverride arguments the previous commit dropped; CI's typecheck job caught the three sites this repo's own tsc -p packages/onboarding pass didn't cover.
* Add test for workflowDeployBody's catalog-offering shape
* e2e: deploy workflows by catalog offering ids
The native workflow deploy route now requires sourceOfferingIds/
defaultSourceOfferingId resolved against the tenant's own catalog,
rather than a raw provider/baseURL/apiKey source chain. Update
workflowDeployBody accordingly, and extract the zero-cost noop
catalog chain smoke-webhook.test.ts already built (an anthropic-
plugin provider pointed at the hub's noop-inference endpoint) into
a shared seedNoopCatalogOffering harness helper.
Every deploying e2e suite now seeds or resolves a real catalog
offering before deploying: walking-skeleton keeps a never-real
placeholder host (assertNeverRealProvider still guards it), chat
resolves the offering seedCatalog already planted rather than
seeding a second chain, and heartbeat/workbench-digest/smoke-webhook
seed the noop chain via the shared helper. cl-6324-launch-proof's
section-mode deploy now reuses the offering id the proof already
resolved when it narrowed the bench catalog to its pinned model.
* Fix formatting
* Add tests for deploying default workflows by catalog offering ids
seedTenant currently deploys every default workflow with a bare
provider/baseURL/apiKey source, but the hub's deploy route now requires
sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's
catalog. Update the seeding, onboarding, and hub config test suites to
expect the new offering-id based deploy body and provider/model-only
ModelSource shape ahead of the implementation change.
* Seeding: deploy default workflows by catalog offering ids
ensureDeployment posted a bare {provider, baseURL, apiKey, model}
source, but the hub's deploy route requires sourceOfferingIds and a
defaultSourceOfferingId resolved from the tenant's catalog, so seeding
a connect flow's "assistant" workflow 400'd. seedTenant now resolves
the tenant's real catalog offerings (sorted by priority, lowest wins
the default) for a normal workflow, and plants a dedicated noop
catalog offering for a modelSource-pinned one (heartbeat), pointed at
the hub's own noop-inference endpoint. ModelSource shrinks to a bare
provider/model pair — the baseURL/apiKey it used to carry only mattered
for the old deploy body, and callers that still need a key for seeding
a catalog (system-seed's root tenant, config's env-key auto-plant) keep
it on their own local type instead.
* longevity-sim: deploy workflows by catalog offering ids
Another workflowDeployBody caller CI's typecheck job caught: this
stack seeds one catalog offering per real inference target already,
so track each one's id and pass it through to the deploy body
instead of the raw provider/baseURL/apiKey shape.
* Fix formatting: prettier
* Add tests for resolved catalog offerings and modelSourceFor's trimmed signature
Covers the new GET .../catalog/resolved-offerings route (denied grant,
and both directly-owned and ancestor-inherited offerings sorted by
priority) and updates seeding/onboarding test fixtures to the route's
response shape ahead of the implementation change.
* Seed deploys against a tenant's resolved catalog offerings, not just owned ones
resolveRealSourceOfferingIds listed only offerings a tenant owns
directly, missing anything inherited from an ancestor tenant — the
hub's own workflow deployer already resolves that wider,
ancestor-inheriting view via listVisibleOfferings. Adds
GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from
apps/hub, implemented in @corbits/inference-catalog so the deployer and
this route share one source of truth) and points seed at it instead.
Also drops the two long-dead unused parameters from
onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from
@corbits/seeding for the e2e harness to reuse.
* Fix remaining modelSourceFor call sites for its trimmed signature
play.ts, greeting-delivery.test.ts, and local-rip.test.ts each still
called modelSourceFor with the apiKey/baseURLOverride arguments the
previous commit dropped; CI's typecheck job caught the three sites
this repo's own tsc -p packages/onboarding pass didn't cover.
* e2e harness: reuse @corbits/seeding's noop catalog offering chain
harness.ts's seedNoopCatalogOffering duplicated the exact chain
@corbits/seeding's ensureNoopCatalogOffering already plants (CL-7461).
Every deploying suite that pointed the offering at the hub's own real
noop-inference endpoint (heartbeat, smoke-webhook, workbench-digest)
now calls the shared helper instead; walking-skeleton keeps its own
local copy, since it deliberately points the offering at an
unreachable placeholder host rather than the hub's noop-inference
endpoint, which the shared helper does not support.
…CL-7471) (#659) * Add test for workflowDeployBody's catalog-offering shape * e2e: deploy workflows by catalog offering ids The native workflow deploy route now requires sourceOfferingIds/ defaultSourceOfferingId resolved against the tenant's own catalog, rather than a raw provider/baseURL/apiKey source chain. Update workflowDeployBody accordingly, and extract the zero-cost noop catalog chain smoke-webhook.test.ts already built (an anthropic- plugin provider pointed at the hub's noop-inference endpoint) into a shared seedNoopCatalogOffering harness helper. Every deploying e2e suite now seeds or resolves a real catalog offering before deploying: walking-skeleton keeps a never-real placeholder host (assertNeverRealProvider still guards it), chat resolves the offering seedCatalog already planted rather than seeding a second chain, and heartbeat/workbench-digest/smoke-webhook seed the noop chain via the shared helper. cl-6324-launch-proof's section-mode deploy now reuses the offering id the proof already resolved when it narrowed the bench catalog to its pinned model. * Fix formatting * Add tests for deploying default workflows by catalog offering ids seedTenant currently deploys every default workflow with a bare provider/baseURL/apiKey source, but the hub's deploy route now requires sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's catalog. Update the seeding, onboarding, and hub config test suites to expect the new offering-id based deploy body and provider/model-only ModelSource shape ahead of the implementation change. * Seeding: deploy default workflows by catalog offering ids ensureDeployment posted a bare {provider, baseURL, apiKey, model} source, but the hub's deploy route requires sourceOfferingIds and a defaultSourceOfferingId resolved from the tenant's catalog, so seeding a connect flow's "assistant" workflow 400'd. seedTenant now resolves the tenant's real catalog offerings (sorted by priority, lowest wins the default) for a normal workflow, and plants a dedicated noop catalog offering for a modelSource-pinned one (heartbeat), pointed at the hub's own noop-inference endpoint. ModelSource shrinks to a bare provider/model pair — the baseURL/apiKey it used to carry only mattered for the old deploy body, and callers that still need a key for seeding a catalog (system-seed's root tenant, config's env-key auto-plant) keep it on their own local type instead. * longevity-sim: deploy workflows by catalog offering ids Another workflowDeployBody caller CI's typecheck job caught: this stack seeds one catalog offering per real inference target already, so track each one's id and pass it through to the deploy body instead of the raw provider/baseURL/apiKey shape. * Fix formatting: prettier * sandbox-sidecar: add tests for generation-0 ensure/destroy requests The allocation service's initial sidecar_allocation and workflow_probe rows start at generation 0 (@intx/db enforces generation >= 0, not > 0), so the first ensure() for a freshly allocated sidecar must be accepted at generation 0. These tests cover that acceptance, later generations, non-integer/negative rejection, and that stale-generation fencing still rejects an older generation once a newer one has been observed. * sandbox-sidecar: accept the allocation service's initial generation @intx/db's sidecar_allocation and workflow_probe tables both check generation >= 0 and the allocation service records the first row for an allocation at generation 0, but validateEnsureRequest() and validateDestroyRequest() in packages/sandbox-sidecar rejected anything that wasn't strictly positive, and the on-disk fencing schema in state-store.ts required generation > 0. The first ensure() for any newly allocated sidecar was rejected with 'generation must be a positive integer', failing every deploy. Accept generation >= 0 in both validators and the state-store schema; stale-generation fencing in observeEnsure()/observeDestroy() already compares against the stored generation rather than assuming positivity, so it is unaffected. * sandbox-sidecar: fix formatting in provisioner test
… the one path (CL-7466) (#660) * Add tests for the Myra land path with no client-side title lookup Replace the by-title stub expectations in myra-workbench.test.ts and default-agent-workbench.test.ts with assertions that ensure() issues exactly the definition-keyed POST /workbenches create, with no kind=workbench or kind=chat list call in front of it. * Drop the client-side title pre-check in front of the Myra land create createDefaultAgentWorkbench used to list workbench and chat kinds and reuse a title match (plus convert a legacy workbench-kind row) before ever reaching the server's create. That duplicated the dedupe POST /workbenches already does by definitionId (findExistingAgentChat in packages/chat), and left two logics that could disagree. Delete the pre-check: the client always issues the definition-keyed create and trusts the server's find-or-reopen. Drops findWorkbenchByTitle, isWorkbenchTitleMatch, and apps/web's findMyraWorkbench/isMyraWorkbenchTitle, which existed only to serve that pre-check. * Format myra-workbench.test.ts to match prettier's line-wrap rules
* Add test for workflowDeployBody's catalog-offering shape
* e2e: deploy workflows by catalog offering ids
The native workflow deploy route now requires sourceOfferingIds/
defaultSourceOfferingId resolved against the tenant's own catalog,
rather than a raw provider/baseURL/apiKey source chain. Update
workflowDeployBody accordingly, and extract the zero-cost noop
catalog chain smoke-webhook.test.ts already built (an anthropic-
plugin provider pointed at the hub's noop-inference endpoint) into
a shared seedNoopCatalogOffering harness helper.
Every deploying e2e suite now seeds or resolves a real catalog
offering before deploying: walking-skeleton keeps a never-real
placeholder host (assertNeverRealProvider still guards it), chat
resolves the offering seedCatalog already planted rather than
seeding a second chain, and heartbeat/workbench-digest/smoke-webhook
seed the noop chain via the shared helper. cl-6324-launch-proof's
section-mode deploy now reuses the offering id the proof already
resolved when it narrowed the bench catalog to its pinned model.
* Fix formatting
* Add tests for deploying default workflows by catalog offering ids
seedTenant currently deploys every default workflow with a bare
provider/baseURL/apiKey source, but the hub's deploy route now requires
sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's
catalog. Update the seeding, onboarding, and hub config test suites to
expect the new offering-id based deploy body and provider/model-only
ModelSource shape ahead of the implementation change.
* Seeding: deploy default workflows by catalog offering ids
ensureDeployment posted a bare {provider, baseURL, apiKey, model}
source, but the hub's deploy route requires sourceOfferingIds and a
defaultSourceOfferingId resolved from the tenant's catalog, so seeding
a connect flow's "assistant" workflow 400'd. seedTenant now resolves
the tenant's real catalog offerings (sorted by priority, lowest wins
the default) for a normal workflow, and plants a dedicated noop
catalog offering for a modelSource-pinned one (heartbeat), pointed at
the hub's own noop-inference endpoint. ModelSource shrinks to a bare
provider/model pair — the baseURL/apiKey it used to carry only mattered
for the old deploy body, and callers that still need a key for seeding
a catalog (system-seed's root tenant, config's env-key auto-plant) keep
it on their own local type instead.
* longevity-sim: deploy workflows by catalog offering ids
Another workflowDeployBody caller CI's typecheck job caught: this
stack seeds one catalog offering per real inference target already,
so track each one's id and pass it through to the deploy body
instead of the raw provider/baseURL/apiKey shape.
* Fix formatting: prettier
* sandbox-sidecar: add tests for generation-0 ensure/destroy requests
The allocation service's initial sidecar_allocation and workflow_probe
rows start at generation 0 (@intx/db enforces generation >= 0, not >
0), so the first ensure() for a freshly allocated sidecar must be
accepted at generation 0. These tests cover that acceptance, later
generations, non-integer/negative rejection, and that stale-generation
fencing still rejects an older generation once a newer one has been
observed.
* sandbox-sidecar: accept the allocation service's initial generation
@intx/db's sidecar_allocation and workflow_probe tables both check
generation >= 0 and the allocation service records the first row for
an allocation at generation 0, but validateEnsureRequest() and
validateDestroyRequest() in packages/sandbox-sidecar rejected anything
that wasn't strictly positive, and the on-disk fencing schema in
state-store.ts required generation > 0. The first ensure() for any
newly allocated sidecar was rejected with 'generation must be a
positive integer', failing every deploy.
Accept generation >= 0 in both validators and the state-store schema;
stale-generation fencing in observeEnsure()/observeDestroy() already
compares against the stored generation rather than assuming positivity,
so it is unaffected.
* sandbox-sidecar: fix formatting in provisioner test
* Add tests for resolved catalog offerings and modelSourceFor's trimmed signature
Covers the new GET .../catalog/resolved-offerings route (denied grant,
and both directly-owned and ancestor-inherited offerings sorted by
priority) and updates seeding/onboarding test fixtures to the route's
response shape ahead of the implementation change.
* Seed deploys against a tenant's resolved catalog offerings, not just owned ones
resolveRealSourceOfferingIds listed only offerings a tenant owns
directly, missing anything inherited from an ancestor tenant — the
hub's own workflow deployer already resolves that wider,
ancestor-inheriting view via listVisibleOfferings. Adds
GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from
apps/hub, implemented in @corbits/inference-catalog so the deployer and
this route share one source of truth) and points seed at it instead.
Also drops the two long-dead unused parameters from
onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from
@corbits/seeding for the e2e harness to reuse.
* Add a regression test pinning the shared-sidecar auth gap (CL-7472)
At the interchange pin this branch stacks on (692c3106), the hub
authenticates a sidecar's register/reconnect frame only against a
sidecar row that resolves to a live sidecar_allocation (or
workflow_probe) row. Upstream dropped the older "shared" credential
scope this repo's e2e harness used to provision a single, standalone
sidecar identity for, so a provisionSidecar-only row now connects and
is rejected with "invalid token" in a loop rather than ever dialing
in. This test pins that negative so a future re-vendor that silently
restores (or relies on) the old scope surfaces here first.
* e2e: stop pre-provisioning a shared sidecar identity (CL-7472)
Every e2e suite dial-in relied on a single, harness-started sidecar
process registered against a bare `provisionSidecar` identity row.
At pin 692c3106, the hub only accepts a register/reconnect frame from
a sidecar whose identity resolves to a live sidecar_allocation (or
workflow_probe) row; a standalone identity with neither now fails the
handshake and gets rejected with "invalid token" every ~3s, forever
(chat mint 500s, deploys never dial in — CI run 34042907014).
The hub's own process-provisioner already spawns a dedicated,
correctly-allocated sidecar process per deployment on demand — the
same path production already takes for a real workflow run or a
minted agent chat. Every affected suite (walking skeleton, chat,
heartbeat, workbench-digest, smoke-chat, smoke-webhook,
greeting-delivery, channel-reconnect, local-rip, and the isolation
suite) now lets that happen instead of dialing in its own sidecar:
the existing 502/500 retry-until-ready loops already cover the wait,
and diagnostics that used to read the one sidecar process's output
now read the hub's — the process-provisioner backend spawns each
sidecar with its stdio inherited from the hub, so the hub's own
captured output already carries every spawned sidecar's log lines.
`provisionSidecar`/`startSidecar` stay in the harness (documented as
non-authenticating at this pin) for the handful of standalone proof
scripts not yet migrated off a directly-controlled sidecar process.
* ci: upload hub/sidecar e2e logs as a build artifact (CL-7472)
The e2e job's hub/sidecar output was never captured, so the CI run
that surfaced the sidecar disconnect loop (34042907014) had no
sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a
runner-temp directory and upload it on every run, pass or fail.
* e2e: lint fixes
* Add test for workflowDeployBody's catalog-offering shape
* e2e: deploy workflows by catalog offering ids
The native workflow deploy route now requires sourceOfferingIds/
defaultSourceOfferingId resolved against the tenant's own catalog,
rather than a raw provider/baseURL/apiKey source chain. Update
workflowDeployBody accordingly, and extract the zero-cost noop
catalog chain smoke-webhook.test.ts already built (an anthropic-
plugin provider pointed at the hub's noop-inference endpoint) into
a shared seedNoopCatalogOffering harness helper.
Every deploying e2e suite now seeds or resolves a real catalog
offering before deploying: walking-skeleton keeps a never-real
placeholder host (assertNeverRealProvider still guards it), chat
resolves the offering seedCatalog already planted rather than
seeding a second chain, and heartbeat/workbench-digest/smoke-webhook
seed the noop chain via the shared helper. cl-6324-launch-proof's
section-mode deploy now reuses the offering id the proof already
resolved when it narrowed the bench catalog to its pinned model.
* Fix formatting
* Add tests for deploying default workflows by catalog offering ids
seedTenant currently deploys every default workflow with a bare
provider/baseURL/apiKey source, but the hub's deploy route now requires
sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's
catalog. Update the seeding, onboarding, and hub config test suites to
expect the new offering-id based deploy body and provider/model-only
ModelSource shape ahead of the implementation change.
* Seeding: deploy default workflows by catalog offering ids
ensureDeployment posted a bare {provider, baseURL, apiKey, model}
source, but the hub's deploy route requires sourceOfferingIds and a
defaultSourceOfferingId resolved from the tenant's catalog, so seeding
a connect flow's "assistant" workflow 400'd. seedTenant now resolves
the tenant's real catalog offerings (sorted by priority, lowest wins
the default) for a normal workflow, and plants a dedicated noop
catalog offering for a modelSource-pinned one (heartbeat), pointed at
the hub's own noop-inference endpoint. ModelSource shrinks to a bare
provider/model pair — the baseURL/apiKey it used to carry only mattered
for the old deploy body, and callers that still need a key for seeding
a catalog (system-seed's root tenant, config's env-key auto-plant) keep
it on their own local type instead.
* longevity-sim: deploy workflows by catalog offering ids
Another workflowDeployBody caller CI's typecheck job caught: this
stack seeds one catalog offering per real inference target already,
so track each one's id and pass it through to the deploy body
instead of the raw provider/baseURL/apiKey shape.
* Fix formatting: prettier
* sandbox-sidecar: add tests for generation-0 ensure/destroy requests
The allocation service's initial sidecar_allocation and workflow_probe
rows start at generation 0 (@intx/db enforces generation >= 0, not >
0), so the first ensure() for a freshly allocated sidecar must be
accepted at generation 0. These tests cover that acceptance, later
generations, non-integer/negative rejection, and that stale-generation
fencing still rejects an older generation once a newer one has been
observed.
* sandbox-sidecar: accept the allocation service's initial generation
@intx/db's sidecar_allocation and workflow_probe tables both check
generation >= 0 and the allocation service records the first row for
an allocation at generation 0, but validateEnsureRequest() and
validateDestroyRequest() in packages/sandbox-sidecar rejected anything
that wasn't strictly positive, and the on-disk fencing schema in
state-store.ts required generation > 0. The first ensure() for any
newly allocated sidecar was rejected with 'generation must be a
positive integer', failing every deploy.
Accept generation >= 0 in both validators and the state-store schema;
stale-generation fencing in observeEnsure()/observeDestroy() already
compares against the stored generation rather than assuming positivity,
so it is unaffected.
* sandbox-sidecar: fix formatting in provisioner test
* Add tests for resolved catalog offerings and modelSourceFor's trimmed signature
Covers the new GET .../catalog/resolved-offerings route (denied grant,
and both directly-owned and ancestor-inherited offerings sorted by
priority) and updates seeding/onboarding test fixtures to the route's
response shape ahead of the implementation change.
* Seed deploys against a tenant's resolved catalog offerings, not just owned ones
resolveRealSourceOfferingIds listed only offerings a tenant owns
directly, missing anything inherited from an ancestor tenant — the
hub's own workflow deployer already resolves that wider,
ancestor-inheriting view via listVisibleOfferings. Adds
GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from
apps/hub, implemented in @corbits/inference-catalog so the deployer and
this route share one source of truth) and points seed at it instead.
Also drops the two long-dead unused parameters from
onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from
@corbits/seeding for the e2e harness to reuse.
* Add a regression test pinning the shared-sidecar auth gap (CL-7472)
At the interchange pin this branch stacks on (692c3106), the hub
authenticates a sidecar's register/reconnect frame only against a
sidecar row that resolves to a live sidecar_allocation (or
workflow_probe) row. Upstream dropped the older "shared" credential
scope this repo's e2e harness used to provision a single, standalone
sidecar identity for, so a provisionSidecar-only row now connects and
is rejected with "invalid token" in a loop rather than ever dialing
in. This test pins that negative so a future re-vendor that silently
restores (or relies on) the old scope surfaces here first.
* e2e: stop pre-provisioning a shared sidecar identity (CL-7472)
Every e2e suite dial-in relied on a single, harness-started sidecar
process registered against a bare `provisionSidecar` identity row.
At pin 692c3106, the hub only accepts a register/reconnect frame from
a sidecar whose identity resolves to a live sidecar_allocation (or
workflow_probe) row; a standalone identity with neither now fails the
handshake and gets rejected with "invalid token" every ~3s, forever
(chat mint 500s, deploys never dial in — CI run 34042907014).
The hub's own process-provisioner already spawns a dedicated,
correctly-allocated sidecar process per deployment on demand — the
same path production already takes for a real workflow run or a
minted agent chat. Every affected suite (walking skeleton, chat,
heartbeat, workbench-digest, smoke-chat, smoke-webhook,
greeting-delivery, channel-reconnect, local-rip, and the isolation
suite) now lets that happen instead of dialing in its own sidecar:
the existing 502/500 retry-until-ready loops already cover the wait,
and diagnostics that used to read the one sidecar process's output
now read the hub's — the process-provisioner backend spawns each
sidecar with its stdio inherited from the hub, so the hub's own
captured output already carries every spawned sidecar's log lines.
`provisionSidecar`/`startSidecar` stay in the harness (documented as
non-authenticating at this pin) for the handful of standalone proof
scripts not yet migrated off a directly-controlled sidecar process.
* ci: upload hub/sidecar e2e logs as a build artifact (CL-7472)
The e2e job's hub/sidecar output was never captured, so the CI run
that surfaced the sidecar disconnect loop (34042907014) had no
sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a
runner-temp directory and upload it on every run, pass or fail.
* e2e: lint fixes
* Add a test pinning the noop catalog offering's model name
Exports NOOP_CATALOG_MODEL from the e2e harness and adds a unit test
asserting seedNoopCatalogOffering plants a catalog model with that
exact canonicalName, so a future edit can't silently drift the
planted (provider, model) pair away from what a suite's workflow
fixture declares as its inference preference.
* walking-skeleton e2e: match the seeded catalog offering's model
The suite's echo workflow declared an inference preference of
anthropic:claude-sonnet-5, while seedNoopCatalogOffering plants a
catalog offering for anthropic:noop. At the re-vendored intx pin, the
deploy-time capability walk only auto-approves the (provider, model)
pair a step's own agent declares (@intx/workflow-deploy's
pickStepInferenceSource), so the mismatch left neither the preferred
source nor the deploy's defaultSource resolvable against an approved
grant, and the deploy 409ed invalid_workflow before the run could
ever start. Naming the same (provider, model) the seeded offering
serves — like every other e2e suite already does — lets the deploy
resolve a source it can actually approve.
* walking-skeleton e2e: match the seeded catalog offering's model
The suite's echo workflow declared an inference preference of
anthropic:claude-sonnet-5, while seedPlaceholderCatalogOffering
plants a catalog offering for anthropic:noop. The deploy-time
capability walk only auto-approves the (provider, model) pair a
step's own agent declares (@intx/workflow-deploy's
pickStepInferenceSource), so the mismatch left neither the preferred
source nor the deploy's defaultSource resolvable against an approved
grant, and the deploy 409ed invalid_workflow before the run could
ever start. Naming PLACEHOLDER_MODEL — the same (provider, model)
the seeded offering serves — lets the deploy resolve a source it can
actually approve.
* walking-skeleton: drop the literal-asserting test
Per the owner's 2026-09-06 ruling to test only non-obvious owned
logic and never literals, remove the unit test asserting
seedPlaceholderCatalogOffering names PLACEHOLDER_MODEL.
6bdabc8 to
a0032b7
Compare
* Add test for workflowDeployBody's catalog-offering shape
* e2e: deploy workflows by catalog offering ids
The native workflow deploy route now requires sourceOfferingIds/
defaultSourceOfferingId resolved against the tenant's own catalog,
rather than a raw provider/baseURL/apiKey source chain. Update
workflowDeployBody accordingly, and extract the zero-cost noop
catalog chain smoke-webhook.test.ts already built (an anthropic-
plugin provider pointed at the hub's noop-inference endpoint) into
a shared seedNoopCatalogOffering harness helper.
Every deploying e2e suite now seeds or resolves a real catalog
offering before deploying: walking-skeleton keeps a never-real
placeholder host (assertNeverRealProvider still guards it), chat
resolves the offering seedCatalog already planted rather than
seeding a second chain, and heartbeat/workbench-digest/smoke-webhook
seed the noop chain via the shared helper. cl-6324-launch-proof's
section-mode deploy now reuses the offering id the proof already
resolved when it narrowed the bench catalog to its pinned model.
* Fix formatting
* Add tests for deploying default workflows by catalog offering ids
seedTenant currently deploys every default workflow with a bare
provider/baseURL/apiKey source, but the hub's deploy route now requires
sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's
catalog. Update the seeding, onboarding, and hub config test suites to
expect the new offering-id based deploy body and provider/model-only
ModelSource shape ahead of the implementation change.
* Seeding: deploy default workflows by catalog offering ids
ensureDeployment posted a bare {provider, baseURL, apiKey, model}
source, but the hub's deploy route requires sourceOfferingIds and a
defaultSourceOfferingId resolved from the tenant's catalog, so seeding
a connect flow's "assistant" workflow 400'd. seedTenant now resolves
the tenant's real catalog offerings (sorted by priority, lowest wins
the default) for a normal workflow, and plants a dedicated noop
catalog offering for a modelSource-pinned one (heartbeat), pointed at
the hub's own noop-inference endpoint. ModelSource shrinks to a bare
provider/model pair — the baseURL/apiKey it used to carry only mattered
for the old deploy body, and callers that still need a key for seeding
a catalog (system-seed's root tenant, config's env-key auto-plant) keep
it on their own local type instead.
* longevity-sim: deploy workflows by catalog offering ids
Another workflowDeployBody caller CI's typecheck job caught: this
stack seeds one catalog offering per real inference target already,
so track each one's id and pass it through to the deploy body
instead of the raw provider/baseURL/apiKey shape.
* Fix formatting: prettier
* sandbox-sidecar: add tests for generation-0 ensure/destroy requests
The allocation service's initial sidecar_allocation and workflow_probe
rows start at generation 0 (@intx/db enforces generation >= 0, not >
0), so the first ensure() for a freshly allocated sidecar must be
accepted at generation 0. These tests cover that acceptance, later
generations, non-integer/negative rejection, and that stale-generation
fencing still rejects an older generation once a newer one has been
observed.
* sandbox-sidecar: accept the allocation service's initial generation
@intx/db's sidecar_allocation and workflow_probe tables both check
generation >= 0 and the allocation service records the first row for
an allocation at generation 0, but validateEnsureRequest() and
validateDestroyRequest() in packages/sandbox-sidecar rejected anything
that wasn't strictly positive, and the on-disk fencing schema in
state-store.ts required generation > 0. The first ensure() for any
newly allocated sidecar was rejected with 'generation must be a
positive integer', failing every deploy.
Accept generation >= 0 in both validators and the state-store schema;
stale-generation fencing in observeEnsure()/observeDestroy() already
compares against the stored generation rather than assuming positivity,
so it is unaffected.
* sandbox-sidecar: fix formatting in provisioner test
* Add tests for resolved catalog offerings and modelSourceFor's trimmed signature
Covers the new GET .../catalog/resolved-offerings route (denied grant,
and both directly-owned and ancestor-inherited offerings sorted by
priority) and updates seeding/onboarding test fixtures to the route's
response shape ahead of the implementation change.
* Seed deploys against a tenant's resolved catalog offerings, not just owned ones
resolveRealSourceOfferingIds listed only offerings a tenant owns
directly, missing anything inherited from an ancestor tenant — the
hub's own workflow deployer already resolves that wider,
ancestor-inheriting view via listVisibleOfferings. Adds
GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from
apps/hub, implemented in @corbits/inference-catalog so the deployer and
this route share one source of truth) and points seed at it instead.
Also drops the two long-dead unused parameters from
onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from
@corbits/seeding for the e2e harness to reuse.
* Add a regression test pinning the shared-sidecar auth gap (CL-7472)
At the interchange pin this branch stacks on (692c3106), the hub
authenticates a sidecar's register/reconnect frame only against a
sidecar row that resolves to a live sidecar_allocation (or
workflow_probe) row. Upstream dropped the older "shared" credential
scope this repo's e2e harness used to provision a single, standalone
sidecar identity for, so a provisionSidecar-only row now connects and
is rejected with "invalid token" in a loop rather than ever dialing
in. This test pins that negative so a future re-vendor that silently
restores (or relies on) the old scope surfaces here first.
* e2e: stop pre-provisioning a shared sidecar identity (CL-7472)
Every e2e suite dial-in relied on a single, harness-started sidecar
process registered against a bare `provisionSidecar` identity row.
At pin 692c3106, the hub only accepts a register/reconnect frame from
a sidecar whose identity resolves to a live sidecar_allocation (or
workflow_probe) row; a standalone identity with neither now fails the
handshake and gets rejected with "invalid token" every ~3s, forever
(chat mint 500s, deploys never dial in — CI run 34042907014).
The hub's own process-provisioner already spawns a dedicated,
correctly-allocated sidecar process per deployment on demand — the
same path production already takes for a real workflow run or a
minted agent chat. Every affected suite (walking skeleton, chat,
heartbeat, workbench-digest, smoke-chat, smoke-webhook,
greeting-delivery, channel-reconnect, local-rip, and the isolation
suite) now lets that happen instead of dialing in its own sidecar:
the existing 502/500 retry-until-ready loops already cover the wait,
and diagnostics that used to read the one sidecar process's output
now read the hub's — the process-provisioner backend spawns each
sidecar with its stdio inherited from the hub, so the hub's own
captured output already carries every spawned sidecar's log lines.
`provisionSidecar`/`startSidecar` stay in the harness (documented as
non-authenticating at this pin) for the handful of standalone proof
scripts not yet migrated off a directly-controlled sidecar process.
* ci: upload hub/sidecar e2e logs as a build artifact (CL-7472)
The e2e job's hub/sidecar output was never captured, so the CI run
that surfaced the sidecar disconnect loop (34042907014) had no
sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a
runner-temp directory and upload it on every run, pass or fail.
* Add tests for tenant-scoped workbench_launch address resolution (CL-7474)
* Chat: workbench_launch address/stableId lookups are tenant-scoped
readBindingByAddress and resolveLiveByStableId took an address or
stable id and resolved whichever workbench_launch row matched it,
regardless of which tenant that row belonged to. Every caller that
already knows its own tenant (sendMail's requireLive, and
refreshAgentInstanceFromDefinition) now passes it through and gets
undefined instead of another tenant's launch if the two ever
disagree. The one legitimate tenant-agnostic caller — the event
stream handler discovering which tenant an inbound address even
belongs to before it has one to check against — keeps working by
simply omitting the new optional parameter.
Also fixes offeringDigest: it joined catalog offering ids with a
NUL byte, which is not valid UTF-8 to Postgres the moment a tenant's
catalog carries more than one visible offering ("invalid byte
sequence for encoding \"UTF8\": 0x00"), permanently failing the
workbench_launch insert for every such launch. This is what was
failing 4 of scripts/e2e/chat.test.ts's cases under per-allocation
sidecars, where seedCatalog now produces more than one offering.
* Chat: run prettier on the tenant-scoping change
* Chat: tenant-scoped binding reads require the tenant; the one unscoped read is named
* Chat: run prettier on the tenant-scoping test file
…7476) (#667) * Add test for workflowDeployBody's catalog-offering shape * e2e: deploy workflows by catalog offering ids The native workflow deploy route now requires sourceOfferingIds/ defaultSourceOfferingId resolved against the tenant's own catalog, rather than a raw provider/baseURL/apiKey source chain. Update workflowDeployBody accordingly, and extract the zero-cost noop catalog chain smoke-webhook.test.ts already built (an anthropic- plugin provider pointed at the hub's noop-inference endpoint) into a shared seedNoopCatalogOffering harness helper. Every deploying e2e suite now seeds or resolves a real catalog offering before deploying: walking-skeleton keeps a never-real placeholder host (assertNeverRealProvider still guards it), chat resolves the offering seedCatalog already planted rather than seeding a second chain, and heartbeat/workbench-digest/smoke-webhook seed the noop chain via the shared helper. cl-6324-launch-proof's section-mode deploy now reuses the offering id the proof already resolved when it narrowed the bench catalog to its pinned model. * Fix formatting * Add tests for deploying default workflows by catalog offering ids seedTenant currently deploys every default workflow with a bare provider/baseURL/apiKey source, but the hub's deploy route now requires sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's catalog. Update the seeding, onboarding, and hub config test suites to expect the new offering-id based deploy body and provider/model-only ModelSource shape ahead of the implementation change. * Seeding: deploy default workflows by catalog offering ids ensureDeployment posted a bare {provider, baseURL, apiKey, model} source, but the hub's deploy route requires sourceOfferingIds and a defaultSourceOfferingId resolved from the tenant's catalog, so seeding a connect flow's "assistant" workflow 400'd. seedTenant now resolves the tenant's real catalog offerings (sorted by priority, lowest wins the default) for a normal workflow, and plants a dedicated noop catalog offering for a modelSource-pinned one (heartbeat), pointed at the hub's own noop-inference endpoint. ModelSource shrinks to a bare provider/model pair — the baseURL/apiKey it used to carry only mattered for the old deploy body, and callers that still need a key for seeding a catalog (system-seed's root tenant, config's env-key auto-plant) keep it on their own local type instead. * longevity-sim: deploy workflows by catalog offering ids Another workflowDeployBody caller CI's typecheck job caught: this stack seeds one catalog offering per real inference target already, so track each one's id and pass it through to the deploy body instead of the raw provider/baseURL/apiKey shape. * Fix formatting: prettier * sandbox-sidecar: add tests for generation-0 ensure/destroy requests The allocation service's initial sidecar_allocation and workflow_probe rows start at generation 0 (@intx/db enforces generation >= 0, not > 0), so the first ensure() for a freshly allocated sidecar must be accepted at generation 0. These tests cover that acceptance, later generations, non-integer/negative rejection, and that stale-generation fencing still rejects an older generation once a newer one has been observed. * sandbox-sidecar: accept the allocation service's initial generation @intx/db's sidecar_allocation and workflow_probe tables both check generation >= 0 and the allocation service records the first row for an allocation at generation 0, but validateEnsureRequest() and validateDestroyRequest() in packages/sandbox-sidecar rejected anything that wasn't strictly positive, and the on-disk fencing schema in state-store.ts required generation > 0. The first ensure() for any newly allocated sidecar was rejected with 'generation must be a positive integer', failing every deploy. Accept generation >= 0 in both validators and the state-store schema; stale-generation fencing in observeEnsure()/observeDestroy() already compares against the stored generation rather than assuming positivity, so it is unaffected. * sandbox-sidecar: fix formatting in provisioner test * Add tests for resolved catalog offerings and modelSourceFor's trimmed signature Covers the new GET .../catalog/resolved-offerings route (denied grant, and both directly-owned and ancestor-inherited offerings sorted by priority) and updates seeding/onboarding test fixtures to the route's response shape ahead of the implementation change. * Seed deploys against a tenant's resolved catalog offerings, not just owned ones resolveRealSourceOfferingIds listed only offerings a tenant owns directly, missing anything inherited from an ancestor tenant — the hub's own workflow deployer already resolves that wider, ancestor-inheriting view via listVisibleOfferings. Adds GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from apps/hub, implemented in @corbits/inference-catalog so the deployer and this route share one source of truth) and points seed at it instead. Also drops the two long-dead unused parameters from onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from @corbits/seeding for the e2e harness to reuse. * Add a regression test pinning the shared-sidecar auth gap (CL-7472) At the interchange pin this branch stacks on (692c3106), the hub authenticates a sidecar's register/reconnect frame only against a sidecar row that resolves to a live sidecar_allocation (or workflow_probe) row. Upstream dropped the older "shared" credential scope this repo's e2e harness used to provision a single, standalone sidecar identity for, so a provisionSidecar-only row now connects and is rejected with "invalid token" in a loop rather than ever dialing in. This test pins that negative so a future re-vendor that silently restores (or relies on) the old scope surfaces here first. * e2e: stop pre-provisioning a shared sidecar identity (CL-7472) Every e2e suite dial-in relied on a single, harness-started sidecar process registered against a bare `provisionSidecar` identity row. At pin 692c3106, the hub only accepts a register/reconnect frame from a sidecar whose identity resolves to a live sidecar_allocation (or workflow_probe) row; a standalone identity with neither now fails the handshake and gets rejected with "invalid token" every ~3s, forever (chat mint 500s, deploys never dial in — CI run 34042907014). The hub's own process-provisioner already spawns a dedicated, correctly-allocated sidecar process per deployment on demand — the same path production already takes for a real workflow run or a minted agent chat. Every affected suite (walking skeleton, chat, heartbeat, workbench-digest, smoke-chat, smoke-webhook, greeting-delivery, channel-reconnect, local-rip, and the isolation suite) now lets that happen instead of dialing in its own sidecar: the existing 502/500 retry-until-ready loops already cover the wait, and diagnostics that used to read the one sidecar process's output now read the hub's — the process-provisioner backend spawns each sidecar with its stdio inherited from the hub, so the hub's own captured output already carries every spawned sidecar's log lines. `provisionSidecar`/`startSidecar` stay in the harness (documented as non-authenticating at this pin) for the handful of standalone proof scripts not yet migrated off a directly-controlled sidecar process. * ci: upload hub/sidecar e2e logs as a build artifact (CL-7472) The e2e job's hub/sidecar output was never captured, so the CI run that surfaced the sidecar disconnect loop (34042907014) had no sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a runner-temp directory and upload it on every run, pass or fail. * Add tests for tenant-scoped workbench_launch address resolution (CL-7474) * Chat: workbench_launch address/stableId lookups are tenant-scoped readBindingByAddress and resolveLiveByStableId took an address or stable id and resolved whichever workbench_launch row matched it, regardless of which tenant that row belonged to. Every caller that already knows its own tenant (sendMail's requireLive, and refreshAgentInstanceFromDefinition) now passes it through and gets undefined instead of another tenant's launch if the two ever disagree. The one legitimate tenant-agnostic caller — the event stream handler discovering which tenant an inbound address even belongs to before it has one to check against — keeps working by simply omitting the new optional parameter. Also fixes offeringDigest: it joined catalog offering ids with a NUL byte, which is not valid UTF-8 to Postgres the moment a tenant's catalog carries more than one visible offering ("invalid byte sequence for encoding \"UTF8\": 0x00"), permanently failing the workbench_launch insert for every such launch. This is what was failing 4 of scripts/e2e/chat.test.ts's cases under per-allocation sidecars, where seedCatalog now produces more than one offering. * Chat: run prettier on the tenant-scoping change * Chat: tenant-scoped binding reads require the tenant; the one unscoped read is named * Add tests for the shared retry-until-routable delivery helper * Deliver a webhook trigger's opening mail once its sidecar becomes routable (CL-7476) * Log a skip, not an error, when root tenant seed has no operator key configured * Run prettier * workflows: say which callers use deliverWhenRoutable
…st (CL-7477) (#668) * Add test for workflowDeployBody's catalog-offering shape * e2e: deploy workflows by catalog offering ids The native workflow deploy route now requires sourceOfferingIds/ defaultSourceOfferingId resolved against the tenant's own catalog, rather than a raw provider/baseURL/apiKey source chain. Update workflowDeployBody accordingly, and extract the zero-cost noop catalog chain smoke-webhook.test.ts already built (an anthropic- plugin provider pointed at the hub's noop-inference endpoint) into a shared seedNoopCatalogOffering harness helper. Every deploying e2e suite now seeds or resolves a real catalog offering before deploying: walking-skeleton keeps a never-real placeholder host (assertNeverRealProvider still guards it), chat resolves the offering seedCatalog already planted rather than seeding a second chain, and heartbeat/workbench-digest/smoke-webhook seed the noop chain via the shared helper. cl-6324-launch-proof's section-mode deploy now reuses the offering id the proof already resolved when it narrowed the bench catalog to its pinned model. * Fix formatting * Add tests for deploying default workflows by catalog offering ids seedTenant currently deploys every default workflow with a bare provider/baseURL/apiKey source, but the hub's deploy route now requires sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's catalog. Update the seeding, onboarding, and hub config test suites to expect the new offering-id based deploy body and provider/model-only ModelSource shape ahead of the implementation change. * Seeding: deploy default workflows by catalog offering ids ensureDeployment posted a bare {provider, baseURL, apiKey, model} source, but the hub's deploy route requires sourceOfferingIds and a defaultSourceOfferingId resolved from the tenant's catalog, so seeding a connect flow's "assistant" workflow 400'd. seedTenant now resolves the tenant's real catalog offerings (sorted by priority, lowest wins the default) for a normal workflow, and plants a dedicated noop catalog offering for a modelSource-pinned one (heartbeat), pointed at the hub's own noop-inference endpoint. ModelSource shrinks to a bare provider/model pair — the baseURL/apiKey it used to carry only mattered for the old deploy body, and callers that still need a key for seeding a catalog (system-seed's root tenant, config's env-key auto-plant) keep it on their own local type instead. * longevity-sim: deploy workflows by catalog offering ids Another workflowDeployBody caller CI's typecheck job caught: this stack seeds one catalog offering per real inference target already, so track each one's id and pass it through to the deploy body instead of the raw provider/baseURL/apiKey shape. * Fix formatting: prettier * sandbox-sidecar: add tests for generation-0 ensure/destroy requests The allocation service's initial sidecar_allocation and workflow_probe rows start at generation 0 (@intx/db enforces generation >= 0, not > 0), so the first ensure() for a freshly allocated sidecar must be accepted at generation 0. These tests cover that acceptance, later generations, non-integer/negative rejection, and that stale-generation fencing still rejects an older generation once a newer one has been observed. * sandbox-sidecar: accept the allocation service's initial generation @intx/db's sidecar_allocation and workflow_probe tables both check generation >= 0 and the allocation service records the first row for an allocation at generation 0, but validateEnsureRequest() and validateDestroyRequest() in packages/sandbox-sidecar rejected anything that wasn't strictly positive, and the on-disk fencing schema in state-store.ts required generation > 0. The first ensure() for any newly allocated sidecar was rejected with 'generation must be a positive integer', failing every deploy. Accept generation >= 0 in both validators and the state-store schema; stale-generation fencing in observeEnsure()/observeDestroy() already compares against the stored generation rather than assuming positivity, so it is unaffected. * sandbox-sidecar: fix formatting in provisioner test * Add tests for resolved catalog offerings and modelSourceFor's trimmed signature Covers the new GET .../catalog/resolved-offerings route (denied grant, and both directly-owned and ancestor-inherited offerings sorted by priority) and updates seeding/onboarding test fixtures to the route's response shape ahead of the implementation change. * Seed deploys against a tenant's resolved catalog offerings, not just owned ones resolveRealSourceOfferingIds listed only offerings a tenant owns directly, missing anything inherited from an ancestor tenant — the hub's own workflow deployer already resolves that wider, ancestor-inheriting view via listVisibleOfferings. Adds GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from apps/hub, implemented in @corbits/inference-catalog so the deployer and this route share one source of truth) and points seed at it instead. Also drops the two long-dead unused parameters from onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from @corbits/seeding for the e2e harness to reuse. * Add a regression test pinning the shared-sidecar auth gap (CL-7472) At the interchange pin this branch stacks on (692c3106), the hub authenticates a sidecar's register/reconnect frame only against a sidecar row that resolves to a live sidecar_allocation (or workflow_probe) row. Upstream dropped the older "shared" credential scope this repo's e2e harness used to provision a single, standalone sidecar identity for, so a provisionSidecar-only row now connects and is rejected with "invalid token" in a loop rather than ever dialing in. This test pins that negative so a future re-vendor that silently restores (or relies on) the old scope surfaces here first. * e2e: stop pre-provisioning a shared sidecar identity (CL-7472) Every e2e suite dial-in relied on a single, harness-started sidecar process registered against a bare `provisionSidecar` identity row. At pin 692c3106, the hub only accepts a register/reconnect frame from a sidecar whose identity resolves to a live sidecar_allocation (or workflow_probe) row; a standalone identity with neither now fails the handshake and gets rejected with "invalid token" every ~3s, forever (chat mint 500s, deploys never dial in — CI run 34042907014). The hub's own process-provisioner already spawns a dedicated, correctly-allocated sidecar process per deployment on demand — the same path production already takes for a real workflow run or a minted agent chat. Every affected suite (walking skeleton, chat, heartbeat, workbench-digest, smoke-chat, smoke-webhook, greeting-delivery, channel-reconnect, local-rip, and the isolation suite) now lets that happen instead of dialing in its own sidecar: the existing 502/500 retry-until-ready loops already cover the wait, and diagnostics that used to read the one sidecar process's output now read the hub's — the process-provisioner backend spawns each sidecar with its stdio inherited from the hub, so the hub's own captured output already carries every spawned sidecar's log lines. `provisionSidecar`/`startSidecar` stay in the harness (documented as non-authenticating at this pin) for the handful of standalone proof scripts not yet migrated off a directly-controlled sidecar process. * ci: upload hub/sidecar e2e logs as a build artifact (CL-7472) The e2e job's hub/sidecar output was never captured, so the CI run that surfaced the sidecar disconnect loop (34042907014) had no sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a runner-temp directory and upload it on every run, pass or fail. * Add tests for tenant-scoped workbench_launch address resolution (CL-7474) * Chat: workbench_launch address/stableId lookups are tenant-scoped readBindingByAddress and resolveLiveByStableId took an address or stable id and resolved whichever workbench_launch row matched it, regardless of which tenant that row belonged to. Every caller that already knows its own tenant (sendMail's requireLive, and refreshAgentInstanceFromDefinition) now passes it through and gets undefined instead of another tenant's launch if the two ever disagree. The one legitimate tenant-agnostic caller — the event stream handler discovering which tenant an inbound address even belongs to before it has one to check against — keeps working by simply omitting the new optional parameter. Also fixes offeringDigest: it joined catalog offering ids with a NUL byte, which is not valid UTF-8 to Postgres the moment a tenant's catalog carries more than one visible offering ("invalid byte sequence for encoding \"UTF8\": 0x00"), permanently failing the workbench_launch insert for every such launch. This is what was failing 4 of scripts/e2e/chat.test.ts's cases under per-allocation sidecars, where seedCatalog now produces more than one offering. * Chat: run prettier on the tenant-scoping change * Chat: tenant-scoped binding reads require the tenant; the one unscoped read is named * Add tests for the shared retry-until-routable delivery helper * Deliver a webhook trigger's opening mail once its sidecar becomes routable (CL-7476) * Log a skip, not an error, when root tenant seed has no operator key configured * Run prettier * Chat: run prettier on the tenant-scoping test file * Add a DB-gated test for recording a run's agent_session (CL-7477) resolveRunSessionId (Interchange's mail-routing lookup) reads agent_session keyed by a run's principal, but nothing writes that table for a native launch since folded-runs was deleted -- a run's first turn silently fails to persist mail. Proves recordAgentSessionForRun writes a row resolveRunSessionId reads back, and updates the webhook launch fake DB to model workflow_run/agent_session the way the fix now touches them. * Native launch records the run's agent_session so mail and spans persist (CL-7477) Before #632 deleted folded-runs, mintFoldedRun was the only writer of agent_session, the table resolveRunSessionId (Interchange's mail-routing lookup, vendor/intx/hub-sessions/src/hub-session-lookups.ts) reads by a run's principal to find its session. Every current native launch path mints its own sessionId and hands it to prepareProvisionedDeployment, but nothing ever wrote it into agent_session -- a run's first turn logged "Endpoint ... has no session for address" and silently dropped its mail. workflow_run.principal_id is still null the instant prepareProvisionedDeployment returns: a provisioned anchor is born "deployed" with no principal, and only the run's first trigger reconciles one onto it (Interchange's anchorWithPrincipal), through the exact same trigger path every sendUserMessage drives. So the write has to land after that first send, not right after provisioning. @corbits/workflows now exports recordAgentSessionForRun, a shared helper that reads a run's tenant/definition/principal fresh off workflow_run and inserts the session row (idempotent, since every send after the first is a no-op). Every call site that sends a message wires it in right after that send: webhook-triggers' launch, agent-directory's one-shot prompt, and chat's sendRunMail (the one chokepoint every chat send -- invite, wake, relaunch -- funnels through, since an invite alone provisions a run with no message and would only ever see a still-null principal). apps/hub's agent-authored-workflow deployer mints a run with no message of its own and its first trigger runs entirely through Interchange's native route with no Workbench hook to record afterward, so it's left alone. Session teardown reuses the same read: chat's relaunch marks the outgoing run's session ended before repointing the room, and the one-shot prompt's teardown ends its own session once its single turn settles.
… (CL-7479) (#669) * Add test for workflowDeployBody's catalog-offering shape * e2e: deploy workflows by catalog offering ids The native workflow deploy route now requires sourceOfferingIds/ defaultSourceOfferingId resolved against the tenant's own catalog, rather than a raw provider/baseURL/apiKey source chain. Update workflowDeployBody accordingly, and extract the zero-cost noop catalog chain smoke-webhook.test.ts already built (an anthropic- plugin provider pointed at the hub's noop-inference endpoint) into a shared seedNoopCatalogOffering harness helper. Every deploying e2e suite now seeds or resolves a real catalog offering before deploying: walking-skeleton keeps a never-real placeholder host (assertNeverRealProvider still guards it), chat resolves the offering seedCatalog already planted rather than seeding a second chain, and heartbeat/workbench-digest/smoke-webhook seed the noop chain via the shared helper. cl-6324-launch-proof's section-mode deploy now reuses the offering id the proof already resolved when it narrowed the bench catalog to its pinned model. * Fix formatting * Add tests for deploying default workflows by catalog offering ids seedTenant currently deploys every default workflow with a bare provider/baseURL/apiKey source, but the hub's deploy route now requires sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's catalog. Update the seeding, onboarding, and hub config test suites to expect the new offering-id based deploy body and provider/model-only ModelSource shape ahead of the implementation change. * Seeding: deploy default workflows by catalog offering ids ensureDeployment posted a bare {provider, baseURL, apiKey, model} source, but the hub's deploy route requires sourceOfferingIds and a defaultSourceOfferingId resolved from the tenant's catalog, so seeding a connect flow's "assistant" workflow 400'd. seedTenant now resolves the tenant's real catalog offerings (sorted by priority, lowest wins the default) for a normal workflow, and plants a dedicated noop catalog offering for a modelSource-pinned one (heartbeat), pointed at the hub's own noop-inference endpoint. ModelSource shrinks to a bare provider/model pair — the baseURL/apiKey it used to carry only mattered for the old deploy body, and callers that still need a key for seeding a catalog (system-seed's root tenant, config's env-key auto-plant) keep it on their own local type instead. * longevity-sim: deploy workflows by catalog offering ids Another workflowDeployBody caller CI's typecheck job caught: this stack seeds one catalog offering per real inference target already, so track each one's id and pass it through to the deploy body instead of the raw provider/baseURL/apiKey shape. * Fix formatting: prettier * sandbox-sidecar: add tests for generation-0 ensure/destroy requests The allocation service's initial sidecar_allocation and workflow_probe rows start at generation 0 (@intx/db enforces generation >= 0, not > 0), so the first ensure() for a freshly allocated sidecar must be accepted at generation 0. These tests cover that acceptance, later generations, non-integer/negative rejection, and that stale-generation fencing still rejects an older generation once a newer one has been observed. * sandbox-sidecar: accept the allocation service's initial generation @intx/db's sidecar_allocation and workflow_probe tables both check generation >= 0 and the allocation service records the first row for an allocation at generation 0, but validateEnsureRequest() and validateDestroyRequest() in packages/sandbox-sidecar rejected anything that wasn't strictly positive, and the on-disk fencing schema in state-store.ts required generation > 0. The first ensure() for any newly allocated sidecar was rejected with 'generation must be a positive integer', failing every deploy. Accept generation >= 0 in both validators and the state-store schema; stale-generation fencing in observeEnsure()/observeDestroy() already compares against the stored generation rather than assuming positivity, so it is unaffected. * sandbox-sidecar: fix formatting in provisioner test * Add tests for resolved catalog offerings and modelSourceFor's trimmed signature Covers the new GET .../catalog/resolved-offerings route (denied grant, and both directly-owned and ancestor-inherited offerings sorted by priority) and updates seeding/onboarding test fixtures to the route's response shape ahead of the implementation change. * Seed deploys against a tenant's resolved catalog offerings, not just owned ones resolveRealSourceOfferingIds listed only offerings a tenant owns directly, missing anything inherited from an ancestor tenant — the hub's own workflow deployer already resolves that wider, ancestor-inheriting view via listVisibleOfferings. Adds GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from apps/hub, implemented in @corbits/inference-catalog so the deployer and this route share one source of truth) and points seed at it instead. Also drops the two long-dead unused parameters from onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from @corbits/seeding for the e2e harness to reuse. * Add a regression test pinning the shared-sidecar auth gap (CL-7472) At the interchange pin this branch stacks on (692c3106), the hub authenticates a sidecar's register/reconnect frame only against a sidecar row that resolves to a live sidecar_allocation (or workflow_probe) row. Upstream dropped the older "shared" credential scope this repo's e2e harness used to provision a single, standalone sidecar identity for, so a provisionSidecar-only row now connects and is rejected with "invalid token" in a loop rather than ever dialing in. This test pins that negative so a future re-vendor that silently restores (or relies on) the old scope surfaces here first. * e2e: stop pre-provisioning a shared sidecar identity (CL-7472) Every e2e suite dial-in relied on a single, harness-started sidecar process registered against a bare `provisionSidecar` identity row. At pin 692c3106, the hub only accepts a register/reconnect frame from a sidecar whose identity resolves to a live sidecar_allocation (or workflow_probe) row; a standalone identity with neither now fails the handshake and gets rejected with "invalid token" every ~3s, forever (chat mint 500s, deploys never dial in — CI run 34042907014). The hub's own process-provisioner already spawns a dedicated, correctly-allocated sidecar process per deployment on demand — the same path production already takes for a real workflow run or a minted agent chat. Every affected suite (walking skeleton, chat, heartbeat, workbench-digest, smoke-chat, smoke-webhook, greeting-delivery, channel-reconnect, local-rip, and the isolation suite) now lets that happen instead of dialing in its own sidecar: the existing 502/500 retry-until-ready loops already cover the wait, and diagnostics that used to read the one sidecar process's output now read the hub's — the process-provisioner backend spawns each sidecar with its stdio inherited from the hub, so the hub's own captured output already carries every spawned sidecar's log lines. `provisionSidecar`/`startSidecar` stay in the harness (documented as non-authenticating at this pin) for the handful of standalone proof scripts not yet migrated off a directly-controlled sidecar process. * ci: upload hub/sidecar e2e logs as a build artifact (CL-7472) The e2e job's hub/sidecar output was never captured, so the CI run that surfaced the sidecar disconnect loop (34042907014) had no sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a runner-temp directory and upload it on every run, pass or fail. * Add tests for tenant-scoped workbench_launch address resolution (CL-7474) * Chat: workbench_launch address/stableId lookups are tenant-scoped readBindingByAddress and resolveLiveByStableId took an address or stable id and resolved whichever workbench_launch row matched it, regardless of which tenant that row belonged to. Every caller that already knows its own tenant (sendMail's requireLive, and refreshAgentInstanceFromDefinition) now passes it through and gets undefined instead of another tenant's launch if the two ever disagree. The one legitimate tenant-agnostic caller — the event stream handler discovering which tenant an inbound address even belongs to before it has one to check against — keeps working by simply omitting the new optional parameter. Also fixes offeringDigest: it joined catalog offering ids with a NUL byte, which is not valid UTF-8 to Postgres the moment a tenant's catalog carries more than one visible offering ("invalid byte sequence for encoding \"UTF8\": 0x00"), permanently failing the workbench_launch insert for every such launch. This is what was failing 4 of scripts/e2e/chat.test.ts's cases under per-allocation sidecars, where seedCatalog now produces more than one offering. * Chat: run prettier on the tenant-scoping change * Chat: tenant-scoped binding reads require the tenant; the one unscoped read is named * Add tests for the shared retry-until-routable delivery helper * Deliver a webhook trigger's opening mail once its sidecar becomes routable (CL-7476) * Log a skip, not an error, when root tenant seed has no operator key configured * Run prettier * Chat: run prettier on the tenant-scoping test file * Add a DB-gated test for recording a run's agent_session (CL-7477) resolveRunSessionId (Interchange's mail-routing lookup) reads agent_session keyed by a run's principal, but nothing writes that table for a native launch since folded-runs was deleted -- a run's first turn silently fails to persist mail. Proves recordAgentSessionForRun writes a row resolveRunSessionId reads back, and updates the webhook launch fake DB to model workflow_run/agent_session the way the fix now touches them. * Native launch records the run's agent_session so mail and spans persist (CL-7477) Before #632 deleted folded-runs, mintFoldedRun was the only writer of agent_session, the table resolveRunSessionId (Interchange's mail-routing lookup, vendor/intx/hub-sessions/src/hub-session-lookups.ts) reads by a run's principal to find its session. Every current native launch path mints its own sessionId and hands it to prepareProvisionedDeployment, but nothing ever wrote it into agent_session -- a run's first turn logged "Endpoint ... has no session for address" and silently dropped its mail. workflow_run.principal_id is still null the instant prepareProvisionedDeployment returns: a provisioned anchor is born "deployed" with no principal, and only the run's first trigger reconciles one onto it (Interchange's anchorWithPrincipal), through the exact same trigger path every sendUserMessage drives. So the write has to land after that first send, not right after provisioning. @corbits/workflows now exports recordAgentSessionForRun, a shared helper that reads a run's tenant/definition/principal fresh off workflow_run and inserts the session row (idempotent, since every send after the first is a no-op). Every call site that sends a message wires it in right after that send: webhook-triggers' launch, agent-directory's one-shot prompt, and chat's sendRunMail (the one chokepoint every chat send -- invite, wake, relaunch -- funnels through, since an invite alone provisions a run with no message and would only ever see a still-null principal). apps/hub's agent-authored-workflow deployer mints a run with no message of its own and its first trigger runs entirely through Interchange's native route with no Workbench hook to record afterward, so it's left alone. Session teardown reuses the same read: chat's relaunch marks the outgoing run's session ended before repointing the room, and the one-shot prompt's teardown ends its own session once its single turn settles. * Add tests for the run's event collector create/abandon wiring * Native launch creates the run's event collector so turns are recorded (CL-7479)
…rs the run (CL-7481) (#671) * Add test for workflowDeployBody's catalog-offering shape * e2e: deploy workflows by catalog offering ids The native workflow deploy route now requires sourceOfferingIds/ defaultSourceOfferingId resolved against the tenant's own catalog, rather than a raw provider/baseURL/apiKey source chain. Update workflowDeployBody accordingly, and extract the zero-cost noop catalog chain smoke-webhook.test.ts already built (an anthropic- plugin provider pointed at the hub's noop-inference endpoint) into a shared seedNoopCatalogOffering harness helper. Every deploying e2e suite now seeds or resolves a real catalog offering before deploying: walking-skeleton keeps a never-real placeholder host (assertNeverRealProvider still guards it), chat resolves the offering seedCatalog already planted rather than seeding a second chain, and heartbeat/workbench-digest/smoke-webhook seed the noop chain via the shared helper. cl-6324-launch-proof's section-mode deploy now reuses the offering id the proof already resolved when it narrowed the bench catalog to its pinned model. * Fix formatting * Add tests for deploying default workflows by catalog offering ids seedTenant currently deploys every default workflow with a bare provider/baseURL/apiKey source, but the hub's deploy route now requires sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's catalog. Update the seeding, onboarding, and hub config test suites to expect the new offering-id based deploy body and provider/model-only ModelSource shape ahead of the implementation change. * Seeding: deploy default workflows by catalog offering ids ensureDeployment posted a bare {provider, baseURL, apiKey, model} source, but the hub's deploy route requires sourceOfferingIds and a defaultSourceOfferingId resolved from the tenant's catalog, so seeding a connect flow's "assistant" workflow 400'd. seedTenant now resolves the tenant's real catalog offerings (sorted by priority, lowest wins the default) for a normal workflow, and plants a dedicated noop catalog offering for a modelSource-pinned one (heartbeat), pointed at the hub's own noop-inference endpoint. ModelSource shrinks to a bare provider/model pair — the baseURL/apiKey it used to carry only mattered for the old deploy body, and callers that still need a key for seeding a catalog (system-seed's root tenant, config's env-key auto-plant) keep it on their own local type instead. * longevity-sim: deploy workflows by catalog offering ids Another workflowDeployBody caller CI's typecheck job caught: this stack seeds one catalog offering per real inference target already, so track each one's id and pass it through to the deploy body instead of the raw provider/baseURL/apiKey shape. * Fix formatting: prettier * sandbox-sidecar: add tests for generation-0 ensure/destroy requests The allocation service's initial sidecar_allocation and workflow_probe rows start at generation 0 (@intx/db enforces generation >= 0, not > 0), so the first ensure() for a freshly allocated sidecar must be accepted at generation 0. These tests cover that acceptance, later generations, non-integer/negative rejection, and that stale-generation fencing still rejects an older generation once a newer one has been observed. * sandbox-sidecar: accept the allocation service's initial generation @intx/db's sidecar_allocation and workflow_probe tables both check generation >= 0 and the allocation service records the first row for an allocation at generation 0, but validateEnsureRequest() and validateDestroyRequest() in packages/sandbox-sidecar rejected anything that wasn't strictly positive, and the on-disk fencing schema in state-store.ts required generation > 0. The first ensure() for any newly allocated sidecar was rejected with 'generation must be a positive integer', failing every deploy. Accept generation >= 0 in both validators and the state-store schema; stale-generation fencing in observeEnsure()/observeDestroy() already compares against the stored generation rather than assuming positivity, so it is unaffected. * sandbox-sidecar: fix formatting in provisioner test * Add tests for resolved catalog offerings and modelSourceFor's trimmed signature Covers the new GET .../catalog/resolved-offerings route (denied grant, and both directly-owned and ancestor-inherited offerings sorted by priority) and updates seeding/onboarding test fixtures to the route's response shape ahead of the implementation change. * Seed deploys against a tenant's resolved catalog offerings, not just owned ones resolveRealSourceOfferingIds listed only offerings a tenant owns directly, missing anything inherited from an ancestor tenant — the hub's own workflow deployer already resolves that wider, ancestor-inheriting view via listVisibleOfferings. Adds GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from apps/hub, implemented in @corbits/inference-catalog so the deployer and this route share one source of truth) and points seed at it instead. Also drops the two long-dead unused parameters from onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from @corbits/seeding for the e2e harness to reuse. * Add a regression test pinning the shared-sidecar auth gap (CL-7472) At the interchange pin this branch stacks on (692c3106), the hub authenticates a sidecar's register/reconnect frame only against a sidecar row that resolves to a live sidecar_allocation (or workflow_probe) row. Upstream dropped the older "shared" credential scope this repo's e2e harness used to provision a single, standalone sidecar identity for, so a provisionSidecar-only row now connects and is rejected with "invalid token" in a loop rather than ever dialing in. This test pins that negative so a future re-vendor that silently restores (or relies on) the old scope surfaces here first. * e2e: stop pre-provisioning a shared sidecar identity (CL-7472) Every e2e suite dial-in relied on a single, harness-started sidecar process registered against a bare `provisionSidecar` identity row. At pin 692c3106, the hub only accepts a register/reconnect frame from a sidecar whose identity resolves to a live sidecar_allocation (or workflow_probe) row; a standalone identity with neither now fails the handshake and gets rejected with "invalid token" every ~3s, forever (chat mint 500s, deploys never dial in — CI run 34042907014). The hub's own process-provisioner already spawns a dedicated, correctly-allocated sidecar process per deployment on demand — the same path production already takes for a real workflow run or a minted agent chat. Every affected suite (walking skeleton, chat, heartbeat, workbench-digest, smoke-chat, smoke-webhook, greeting-delivery, channel-reconnect, local-rip, and the isolation suite) now lets that happen instead of dialing in its own sidecar: the existing 502/500 retry-until-ready loops already cover the wait, and diagnostics that used to read the one sidecar process's output now read the hub's — the process-provisioner backend spawns each sidecar with its stdio inherited from the hub, so the hub's own captured output already carries every spawned sidecar's log lines. `provisionSidecar`/`startSidecar` stay in the harness (documented as non-authenticating at this pin) for the handful of standalone proof scripts not yet migrated off a directly-controlled sidecar process. * ci: upload hub/sidecar e2e logs as a build artifact (CL-7472) The e2e job's hub/sidecar output was never captured, so the CI run that surfaced the sidecar disconnect loop (34042907014) had no sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a runner-temp directory and upload it on every run, pass or fail. * Add tests for tenant-scoped workbench_launch address resolution (CL-7474) * Chat: workbench_launch address/stableId lookups are tenant-scoped readBindingByAddress and resolveLiveByStableId took an address or stable id and resolved whichever workbench_launch row matched it, regardless of which tenant that row belonged to. Every caller that already knows its own tenant (sendMail's requireLive, and refreshAgentInstanceFromDefinition) now passes it through and gets undefined instead of another tenant's launch if the two ever disagree. The one legitimate tenant-agnostic caller — the event stream handler discovering which tenant an inbound address even belongs to before it has one to check against — keeps working by simply omitting the new optional parameter. Also fixes offeringDigest: it joined catalog offering ids with a NUL byte, which is not valid UTF-8 to Postgres the moment a tenant's catalog carries more than one visible offering ("invalid byte sequence for encoding \"UTF8\": 0x00"), permanently failing the workbench_launch insert for every such launch. This is what was failing 4 of scripts/e2e/chat.test.ts's cases under per-allocation sidecars, where seedCatalog now produces more than one offering. * Chat: run prettier on the tenant-scoping change * Chat: tenant-scoped binding reads require the tenant; the one unscoped read is named * Add tests for the shared retry-until-routable delivery helper * Deliver a webhook trigger's opening mail once its sidecar becomes routable (CL-7476) * Log a skip, not an error, when root tenant seed has no operator key configured * Run prettier * Chat: run prettier on the tenant-scoping test file * Add a DB-gated test for recording a run's agent_session (CL-7477) resolveRunSessionId (Interchange's mail-routing lookup) reads agent_session keyed by a run's principal, but nothing writes that table for a native launch since folded-runs was deleted -- a run's first turn silently fails to persist mail. Proves recordAgentSessionForRun writes a row resolveRunSessionId reads back, and updates the webhook launch fake DB to model workflow_run/agent_session the way the fix now touches them. * Native launch records the run's agent_session so mail and spans persist (CL-7477) Before #632 deleted folded-runs, mintFoldedRun was the only writer of agent_session, the table resolveRunSessionId (Interchange's mail-routing lookup, vendor/intx/hub-sessions/src/hub-session-lookups.ts) reads by a run's principal to find its session. Every current native launch path mints its own sessionId and hands it to prepareProvisionedDeployment, but nothing ever wrote it into agent_session -- a run's first turn logged "Endpoint ... has no session for address" and silently dropped its mail. workflow_run.principal_id is still null the instant prepareProvisionedDeployment returns: a provisioned anchor is born "deployed" with no principal, and only the run's first trigger reconciles one onto it (Interchange's anchorWithPrincipal), through the exact same trigger path every sendUserMessage drives. So the write has to land after that first send, not right after provisioning. @corbits/workflows now exports recordAgentSessionForRun, a shared helper that reads a run's tenant/definition/principal fresh off workflow_run and inserts the session row (idempotent, since every send after the first is a no-op). Every call site that sends a message wires it in right after that send: webhook-triggers' launch, agent-directory's one-shot prompt, and chat's sendRunMail (the one chokepoint every chat send -- invite, wake, relaunch -- funnels through, since an invite alone provisions a run with no message and would only ever see a still-null principal). apps/hub's agent-authored-workflow deployer mints a run with no message of its own and its first trigger runs entirely through Interchange's native route with no Workbench hook to record afterward, so it's left alone. Session teardown reuses the same read: chat's relaunch marks the outgoing run's session ended before repointing the room, and the one-shot prompt's teardown ends its own session once its single turn settles. * Add tests for the run's event collector create/abandon wiring * Native launch creates the run's event collector so turns are recorded (CL-7479) * Add tests for lazy, idempotent run-session ensure (CL-7480) * Hub ensures a run's session and collector on first use (CL-7480) Replaces the post-send recordAgentSessionForRun (CL-7477/CL-7479) with one idempotent ensureRunSession, called lazily at the hub seams that actually see a run become mail-routable: the mailbox persist wrapper and the event-collector dispatch path, plus chat's pre-send mail dispatch. Pre-creating the run's principal to dodge the timing isn't an option -- Interchange's grant materialization treats a conflict on that insert as grants already committed and throws. * Fix prettier formatting in agent-session.ts (CL-7480) * Add tests for eager session recording and launch-spec session lookup * Record a run's session at provision; re-key it when Interchange anchors the run * Fix CI: prettier formatting, principal enum value, and workflowRunLaunchSpec fakes
… routable (CL-7487) (#676) * Add test for workflowDeployBody's catalog-offering shape * e2e: deploy workflows by catalog offering ids The native workflow deploy route now requires sourceOfferingIds/ defaultSourceOfferingId resolved against the tenant's own catalog, rather than a raw provider/baseURL/apiKey source chain. Update workflowDeployBody accordingly, and extract the zero-cost noop catalog chain smoke-webhook.test.ts already built (an anthropic- plugin provider pointed at the hub's noop-inference endpoint) into a shared seedNoopCatalogOffering harness helper. Every deploying e2e suite now seeds or resolves a real catalog offering before deploying: walking-skeleton keeps a never-real placeholder host (assertNeverRealProvider still guards it), chat resolves the offering seedCatalog already planted rather than seeding a second chain, and heartbeat/workbench-digest/smoke-webhook seed the noop chain via the shared helper. cl-6324-launch-proof's section-mode deploy now reuses the offering id the proof already resolved when it narrowed the bench catalog to its pinned model. * Fix formatting * Add tests for deploying default workflows by catalog offering ids seedTenant currently deploys every default workflow with a bare provider/baseURL/apiKey source, but the hub's deploy route now requires sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's catalog. Update the seeding, onboarding, and hub config test suites to expect the new offering-id based deploy body and provider/model-only ModelSource shape ahead of the implementation change. * Seeding: deploy default workflows by catalog offering ids ensureDeployment posted a bare {provider, baseURL, apiKey, model} source, but the hub's deploy route requires sourceOfferingIds and a defaultSourceOfferingId resolved from the tenant's catalog, so seeding a connect flow's "assistant" workflow 400'd. seedTenant now resolves the tenant's real catalog offerings (sorted by priority, lowest wins the default) for a normal workflow, and plants a dedicated noop catalog offering for a modelSource-pinned one (heartbeat), pointed at the hub's own noop-inference endpoint. ModelSource shrinks to a bare provider/model pair — the baseURL/apiKey it used to carry only mattered for the old deploy body, and callers that still need a key for seeding a catalog (system-seed's root tenant, config's env-key auto-plant) keep it on their own local type instead. * longevity-sim: deploy workflows by catalog offering ids Another workflowDeployBody caller CI's typecheck job caught: this stack seeds one catalog offering per real inference target already, so track each one's id and pass it through to the deploy body instead of the raw provider/baseURL/apiKey shape. * Fix formatting: prettier * sandbox-sidecar: add tests for generation-0 ensure/destroy requests The allocation service's initial sidecar_allocation and workflow_probe rows start at generation 0 (@intx/db enforces generation >= 0, not > 0), so the first ensure() for a freshly allocated sidecar must be accepted at generation 0. These tests cover that acceptance, later generations, non-integer/negative rejection, and that stale-generation fencing still rejects an older generation once a newer one has been observed. * sandbox-sidecar: accept the allocation service's initial generation @intx/db's sidecar_allocation and workflow_probe tables both check generation >= 0 and the allocation service records the first row for an allocation at generation 0, but validateEnsureRequest() and validateDestroyRequest() in packages/sandbox-sidecar rejected anything that wasn't strictly positive, and the on-disk fencing schema in state-store.ts required generation > 0. The first ensure() for any newly allocated sidecar was rejected with 'generation must be a positive integer', failing every deploy. Accept generation >= 0 in both validators and the state-store schema; stale-generation fencing in observeEnsure()/observeDestroy() already compares against the stored generation rather than assuming positivity, so it is unaffected. * sandbox-sidecar: fix formatting in provisioner test * Add tests for resolved catalog offerings and modelSourceFor's trimmed signature Covers the new GET .../catalog/resolved-offerings route (denied grant, and both directly-owned and ancestor-inherited offerings sorted by priority) and updates seeding/onboarding test fixtures to the route's response shape ahead of the implementation change. * Seed deploys against a tenant's resolved catalog offerings, not just owned ones resolveRealSourceOfferingIds listed only offerings a tenant owns directly, missing anything inherited from an ancestor tenant — the hub's own workflow deployer already resolves that wider, ancestor-inheriting view via listVisibleOfferings. Adds GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from apps/hub, implemented in @corbits/inference-catalog so the deployer and this route share one source of truth) and points seed at it instead. Also drops the two long-dead unused parameters from onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from @corbits/seeding for the e2e harness to reuse. * Add a regression test pinning the shared-sidecar auth gap (CL-7472) At the interchange pin this branch stacks on (692c3106), the hub authenticates a sidecar's register/reconnect frame only against a sidecar row that resolves to a live sidecar_allocation (or workflow_probe) row. Upstream dropped the older "shared" credential scope this repo's e2e harness used to provision a single, standalone sidecar identity for, so a provisionSidecar-only row now connects and is rejected with "invalid token" in a loop rather than ever dialing in. This test pins that negative so a future re-vendor that silently restores (or relies on) the old scope surfaces here first. * e2e: stop pre-provisioning a shared sidecar identity (CL-7472) Every e2e suite dial-in relied on a single, harness-started sidecar process registered against a bare `provisionSidecar` identity row. At pin 692c3106, the hub only accepts a register/reconnect frame from a sidecar whose identity resolves to a live sidecar_allocation (or workflow_probe) row; a standalone identity with neither now fails the handshake and gets rejected with "invalid token" every ~3s, forever (chat mint 500s, deploys never dial in — CI run 34042907014). The hub's own process-provisioner already spawns a dedicated, correctly-allocated sidecar process per deployment on demand — the same path production already takes for a real workflow run or a minted agent chat. Every affected suite (walking skeleton, chat, heartbeat, workbench-digest, smoke-chat, smoke-webhook, greeting-delivery, channel-reconnect, local-rip, and the isolation suite) now lets that happen instead of dialing in its own sidecar: the existing 502/500 retry-until-ready loops already cover the wait, and diagnostics that used to read the one sidecar process's output now read the hub's — the process-provisioner backend spawns each sidecar with its stdio inherited from the hub, so the hub's own captured output already carries every spawned sidecar's log lines. `provisionSidecar`/`startSidecar` stay in the harness (documented as non-authenticating at this pin) for the handful of standalone proof scripts not yet migrated off a directly-controlled sidecar process. * ci: upload hub/sidecar e2e logs as a build artifact (CL-7472) The e2e job's hub/sidecar output was never captured, so the CI run that surfaced the sidecar disconnect loop (34042907014) had no sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a runner-temp directory and upload it on every run, pass or fail. * Add tests for tenant-scoped workbench_launch address resolution (CL-7474) * Chat: workbench_launch address/stableId lookups are tenant-scoped readBindingByAddress and resolveLiveByStableId took an address or stable id and resolved whichever workbench_launch row matched it, regardless of which tenant that row belonged to. Every caller that already knows its own tenant (sendMail's requireLive, and refreshAgentInstanceFromDefinition) now passes it through and gets undefined instead of another tenant's launch if the two ever disagree. The one legitimate tenant-agnostic caller — the event stream handler discovering which tenant an inbound address even belongs to before it has one to check against — keeps working by simply omitting the new optional parameter. Also fixes offeringDigest: it joined catalog offering ids with a NUL byte, which is not valid UTF-8 to Postgres the moment a tenant's catalog carries more than one visible offering ("invalid byte sequence for encoding \"UTF8\": 0x00"), permanently failing the workbench_launch insert for every such launch. This is what was failing 4 of scripts/e2e/chat.test.ts's cases under per-allocation sidecars, where seedCatalog now produces more than one offering. * Chat: run prettier on the tenant-scoping change * Chat: tenant-scoped binding reads require the tenant; the one unscoped read is named * Add tests for the shared retry-until-routable delivery helper * Deliver a webhook trigger's opening mail once its sidecar becomes routable (CL-7476) * Log a skip, not an error, when root tenant seed has no operator key configured * Run prettier * Chat: run prettier on the tenant-scoping test file * Add a DB-gated test for recording a run's agent_session (CL-7477) resolveRunSessionId (Interchange's mail-routing lookup) reads agent_session keyed by a run's principal, but nothing writes that table for a native launch since folded-runs was deleted -- a run's first turn silently fails to persist mail. Proves recordAgentSessionForRun writes a row resolveRunSessionId reads back, and updates the webhook launch fake DB to model workflow_run/agent_session the way the fix now touches them. * Native launch records the run's agent_session so mail and spans persist (CL-7477) Before #632 deleted folded-runs, mintFoldedRun was the only writer of agent_session, the table resolveRunSessionId (Interchange's mail-routing lookup, vendor/intx/hub-sessions/src/hub-session-lookups.ts) reads by a run's principal to find its session. Every current native launch path mints its own sessionId and hands it to prepareProvisionedDeployment, but nothing ever wrote it into agent_session -- a run's first turn logged "Endpoint ... has no session for address" and silently dropped its mail. workflow_run.principal_id is still null the instant prepareProvisionedDeployment returns: a provisioned anchor is born "deployed" with no principal, and only the run's first trigger reconciles one onto it (Interchange's anchorWithPrincipal), through the exact same trigger path every sendUserMessage drives. So the write has to land after that first send, not right after provisioning. @corbits/workflows now exports recordAgentSessionForRun, a shared helper that reads a run's tenant/definition/principal fresh off workflow_run and inserts the session row (idempotent, since every send after the first is a no-op). Every call site that sends a message wires it in right after that send: webhook-triggers' launch, agent-directory's one-shot prompt, and chat's sendRunMail (the one chokepoint every chat send -- invite, wake, relaunch -- funnels through, since an invite alone provisions a run with no message and would only ever see a still-null principal). apps/hub's agent-authored-workflow deployer mints a run with no message of its own and its first trigger runs entirely through Interchange's native route with no Workbench hook to record afterward, so it's left alone. Session teardown reuses the same read: chat's relaunch marks the outgoing run's session ended before repointing the room, and the one-shot prompt's teardown ends its own session once its single turn settles. * Add tests for the run's event collector create/abandon wiring * Native launch creates the run's event collector so turns are recorded (CL-7479) * Add tests for lazy, idempotent run-session ensure (CL-7480) * Hub ensures a run's session and collector on first use (CL-7480) Replaces the post-send recordAgentSessionForRun (CL-7477/CL-7479) with one idempotent ensureRunSession, called lazily at the hub seams that actually see a run become mail-routable: the mailbox persist wrapper and the event-collector dispatch path, plus chat's pre-send mail dispatch. Pre-creating the run's principal to dodge the timing isn't an option -- Interchange's grant materialization treats a conflict on that insert as grants already committed and throws. * Fix prettier formatting in agent-session.ts (CL-7480) * Add tests for eager session recording and launch-spec session lookup * Record a run's session at provision; re-key it when Interchange anchors the run * Fix CI: prettier formatting, principal enum value, and workflowRunLaunchSpec fakes * Fix merge fallout: restore CL-7481 agent-session exports and drop a duplicate key Merging cl-7107's repin/re-vendor audit into this branch left packages/workflows/src/index.ts re-exporting the pre-CL-7481 agent-session API (recordAgentSessionForRun) that no longer exists, its matching test file testing that same stale API, and a duplicated eventCollectors key in apps/hub/src/index.ts's webhook-trigger launch wiring. Restores the current exports (recordAgentSessionAtProvision, ensureRunSession) and their test, and drops the duplicate object key. * Add tests for wake retry re-resolving the live address (CL-7486) sendRunMailWithReclaimRetry retried every attempt against the address it started with. Proves the reproduction of the production failure: a participant already one relaunch generation in that dies again mid-retry must have its next retry chase the run that replaced it, not keep hammering the address that just died. * Chat: wake retry chases the run a relaunch replaced it with (CL-7486) sendRunMailWithReclaimRetry retried every delivery attempt, and woke, against the address it captured at the top of the loop. A wake in between retries can relaunch the run (its dead-run branch repoints the room's stable participant at a fresh address without telling this loop), and a participant already one relaunch generation in that dies again mid-retry leaves this loop holding an address that is no longer either the stable participant id or the current run id — invisible to readBindingByAddressAnyTenant. The next wake for it then threw "No workbench_launch binding" instead of finding the run that replaced it, which is what chat e2e's mention-fanout and invite-drives-a-turn cases were hitting. Re-resolves the live address (and its session) off the stable participant id after each wake, so the retry loop always chases whichever run is actually current. Also hardens publishCorbitsToolsRegistry's create-then-list-on-conflict ensure: two overlapping seed runs racing the same tenant's package registry asset can still throw a raw asset_tenant_kind_name violation if the substrate's own 409 mapping is bypassed; the ensure now also recognizes that violation by name and resolves the winner's row instead of throwing. * Fix webhook-triggers launch test fixture: eventCollectors needs has() * Add test coverage for routable-wait relaunch chasing in sendRunMailWithReclaimRetry Drives the CL-7486 reclaim-retry test off routing-table transitions instead of fixed backoff timing, and updates the CL-7214 coalescing test's fake sidecar router to model a redeploy actually registering. * Chat: wait for the routing table instead of a fixed backoff before giving up on first-turn delivery sendRunMailWithReclaimRetry used to exhaust a ~7.75s backoff and report the invited agent unreachable while its sidecar was still booting and registering with the hub (CL-7476) -- fast enough to make the fan-out relaunch a perfectly healthy run. It now shares deliverWhenRoutable with the webhook trigger's ingress delivery: wake once on the first unreachable failure (still chasing a mid-wait relaunch by re-resolving the live address inside the retry's send closure), then wait on the hub's routing table up to the same boot-and-register budget (DEFAULT_ROUTABLE_DEADLINE_MS, now exported from @corbits/workflows) before retrying exactly once more. * Fix formatting
… throwing (#677) * Add test for workflowDeployBody's catalog-offering shape * e2e: deploy workflows by catalog offering ids The native workflow deploy route now requires sourceOfferingIds/ defaultSourceOfferingId resolved against the tenant's own catalog, rather than a raw provider/baseURL/apiKey source chain. Update workflowDeployBody accordingly, and extract the zero-cost noop catalog chain smoke-webhook.test.ts already built (an anthropic- plugin provider pointed at the hub's noop-inference endpoint) into a shared seedNoopCatalogOffering harness helper. Every deploying e2e suite now seeds or resolves a real catalog offering before deploying: walking-skeleton keeps a never-real placeholder host (assertNeverRealProvider still guards it), chat resolves the offering seedCatalog already planted rather than seeding a second chain, and heartbeat/workbench-digest/smoke-webhook seed the noop chain via the shared helper. cl-6324-launch-proof's section-mode deploy now reuses the offering id the proof already resolved when it narrowed the bench catalog to its pinned model. * Fix formatting * Add tests for deploying default workflows by catalog offering ids seedTenant currently deploys every default workflow with a bare provider/baseURL/apiKey source, but the hub's deploy route now requires sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's catalog. Update the seeding, onboarding, and hub config test suites to expect the new offering-id based deploy body and provider/model-only ModelSource shape ahead of the implementation change. * Seeding: deploy default workflows by catalog offering ids ensureDeployment posted a bare {provider, baseURL, apiKey, model} source, but the hub's deploy route requires sourceOfferingIds and a defaultSourceOfferingId resolved from the tenant's catalog, so seeding a connect flow's "assistant" workflow 400'd. seedTenant now resolves the tenant's real catalog offerings (sorted by priority, lowest wins the default) for a normal workflow, and plants a dedicated noop catalog offering for a modelSource-pinned one (heartbeat), pointed at the hub's own noop-inference endpoint. ModelSource shrinks to a bare provider/model pair — the baseURL/apiKey it used to carry only mattered for the old deploy body, and callers that still need a key for seeding a catalog (system-seed's root tenant, config's env-key auto-plant) keep it on their own local type instead. * longevity-sim: deploy workflows by catalog offering ids Another workflowDeployBody caller CI's typecheck job caught: this stack seeds one catalog offering per real inference target already, so track each one's id and pass it through to the deploy body instead of the raw provider/baseURL/apiKey shape. * Fix formatting: prettier * sandbox-sidecar: add tests for generation-0 ensure/destroy requests The allocation service's initial sidecar_allocation and workflow_probe rows start at generation 0 (@intx/db enforces generation >= 0, not > 0), so the first ensure() for a freshly allocated sidecar must be accepted at generation 0. These tests cover that acceptance, later generations, non-integer/negative rejection, and that stale-generation fencing still rejects an older generation once a newer one has been observed. * sandbox-sidecar: accept the allocation service's initial generation @intx/db's sidecar_allocation and workflow_probe tables both check generation >= 0 and the allocation service records the first row for an allocation at generation 0, but validateEnsureRequest() and validateDestroyRequest() in packages/sandbox-sidecar rejected anything that wasn't strictly positive, and the on-disk fencing schema in state-store.ts required generation > 0. The first ensure() for any newly allocated sidecar was rejected with 'generation must be a positive integer', failing every deploy. Accept generation >= 0 in both validators and the state-store schema; stale-generation fencing in observeEnsure()/observeDestroy() already compares against the stored generation rather than assuming positivity, so it is unaffected. * sandbox-sidecar: fix formatting in provisioner test * Add tests for resolved catalog offerings and modelSourceFor's trimmed signature Covers the new GET .../catalog/resolved-offerings route (denied grant, and both directly-owned and ancestor-inherited offerings sorted by priority) and updates seeding/onboarding test fixtures to the route's response shape ahead of the implementation change. * Seed deploys against a tenant's resolved catalog offerings, not just owned ones resolveRealSourceOfferingIds listed only offerings a tenant owns directly, missing anything inherited from an ancestor tenant — the hub's own workflow deployer already resolves that wider, ancestor-inheriting view via listVisibleOfferings. Adds GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from apps/hub, implemented in @corbits/inference-catalog so the deployer and this route share one source of truth) and points seed at it instead. Also drops the two long-dead unused parameters from onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from @corbits/seeding for the e2e harness to reuse. * Add a regression test pinning the shared-sidecar auth gap (CL-7472) At the interchange pin this branch stacks on (692c3106), the hub authenticates a sidecar's register/reconnect frame only against a sidecar row that resolves to a live sidecar_allocation (or workflow_probe) row. Upstream dropped the older "shared" credential scope this repo's e2e harness used to provision a single, standalone sidecar identity for, so a provisionSidecar-only row now connects and is rejected with "invalid token" in a loop rather than ever dialing in. This test pins that negative so a future re-vendor that silently restores (or relies on) the old scope surfaces here first. * e2e: stop pre-provisioning a shared sidecar identity (CL-7472) Every e2e suite dial-in relied on a single, harness-started sidecar process registered against a bare `provisionSidecar` identity row. At pin 692c3106, the hub only accepts a register/reconnect frame from a sidecar whose identity resolves to a live sidecar_allocation (or workflow_probe) row; a standalone identity with neither now fails the handshake and gets rejected with "invalid token" every ~3s, forever (chat mint 500s, deploys never dial in — CI run 34042907014). The hub's own process-provisioner already spawns a dedicated, correctly-allocated sidecar process per deployment on demand — the same path production already takes for a real workflow run or a minted agent chat. Every affected suite (walking skeleton, chat, heartbeat, workbench-digest, smoke-chat, smoke-webhook, greeting-delivery, channel-reconnect, local-rip, and the isolation suite) now lets that happen instead of dialing in its own sidecar: the existing 502/500 retry-until-ready loops already cover the wait, and diagnostics that used to read the one sidecar process's output now read the hub's — the process-provisioner backend spawns each sidecar with its stdio inherited from the hub, so the hub's own captured output already carries every spawned sidecar's log lines. `provisionSidecar`/`startSidecar` stay in the harness (documented as non-authenticating at this pin) for the handful of standalone proof scripts not yet migrated off a directly-controlled sidecar process. * ci: upload hub/sidecar e2e logs as a build artifact (CL-7472) The e2e job's hub/sidecar output was never captured, so the CI run that surfaced the sidecar disconnect loop (34042907014) had no sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a runner-temp directory and upload it on every run, pass or fail. * Add tests for tenant-scoped workbench_launch address resolution (CL-7474) * Chat: workbench_launch address/stableId lookups are tenant-scoped readBindingByAddress and resolveLiveByStableId took an address or stable id and resolved whichever workbench_launch row matched it, regardless of which tenant that row belonged to. Every caller that already knows its own tenant (sendMail's requireLive, and refreshAgentInstanceFromDefinition) now passes it through and gets undefined instead of another tenant's launch if the two ever disagree. The one legitimate tenant-agnostic caller — the event stream handler discovering which tenant an inbound address even belongs to before it has one to check against — keeps working by simply omitting the new optional parameter. Also fixes offeringDigest: it joined catalog offering ids with a NUL byte, which is not valid UTF-8 to Postgres the moment a tenant's catalog carries more than one visible offering ("invalid byte sequence for encoding \"UTF8\": 0x00"), permanently failing the workbench_launch insert for every such launch. This is what was failing 4 of scripts/e2e/chat.test.ts's cases under per-allocation sidecars, where seedCatalog now produces more than one offering. * Chat: run prettier on the tenant-scoping change * Chat: tenant-scoped binding reads require the tenant; the one unscoped read is named * Add tests for the shared retry-until-routable delivery helper * Deliver a webhook trigger's opening mail once its sidecar becomes routable (CL-7476) * Log a skip, not an error, when root tenant seed has no operator key configured * Run prettier * Chat: run prettier on the tenant-scoping test file * Add a DB-gated test for recording a run's agent_session (CL-7477) resolveRunSessionId (Interchange's mail-routing lookup) reads agent_session keyed by a run's principal, but nothing writes that table for a native launch since folded-runs was deleted -- a run's first turn silently fails to persist mail. Proves recordAgentSessionForRun writes a row resolveRunSessionId reads back, and updates the webhook launch fake DB to model workflow_run/agent_session the way the fix now touches them. * Native launch records the run's agent_session so mail and spans persist (CL-7477) Before #632 deleted folded-runs, mintFoldedRun was the only writer of agent_session, the table resolveRunSessionId (Interchange's mail-routing lookup, vendor/intx/hub-sessions/src/hub-session-lookups.ts) reads by a run's principal to find its session. Every current native launch path mints its own sessionId and hands it to prepareProvisionedDeployment, but nothing ever wrote it into agent_session -- a run's first turn logged "Endpoint ... has no session for address" and silently dropped its mail. workflow_run.principal_id is still null the instant prepareProvisionedDeployment returns: a provisioned anchor is born "deployed" with no principal, and only the run's first trigger reconciles one onto it (Interchange's anchorWithPrincipal), through the exact same trigger path every sendUserMessage drives. So the write has to land after that first send, not right after provisioning. @corbits/workflows now exports recordAgentSessionForRun, a shared helper that reads a run's tenant/definition/principal fresh off workflow_run and inserts the session row (idempotent, since every send after the first is a no-op). Every call site that sends a message wires it in right after that send: webhook-triggers' launch, agent-directory's one-shot prompt, and chat's sendRunMail (the one chokepoint every chat send -- invite, wake, relaunch -- funnels through, since an invite alone provisions a run with no message and would only ever see a still-null principal). apps/hub's agent-authored-workflow deployer mints a run with no message of its own and its first trigger runs entirely through Interchange's native route with no Workbench hook to record afterward, so it's left alone. Session teardown reuses the same read: chat's relaunch marks the outgoing run's session ended before repointing the room, and the one-shot prompt's teardown ends its own session once its single turn settles. * Add tests for the run's event collector create/abandon wiring * Native launch creates the run's event collector so turns are recorded (CL-7479) * Add tests for lazy, idempotent run-session ensure (CL-7480) * Hub ensures a run's session and collector on first use (CL-7480) Replaces the post-send recordAgentSessionForRun (CL-7477/CL-7479) with one idempotent ensureRunSession, called lazily at the hub seams that actually see a run become mail-routable: the mailbox persist wrapper and the event-collector dispatch path, plus chat's pre-send mail dispatch. Pre-creating the run's principal to dodge the timing isn't an option -- Interchange's grant materialization treats a conflict on that insert as grants already committed and throws. * Fix prettier formatting in agent-session.ts (CL-7480) * Add tests for eager session recording and launch-spec session lookup * Record a run's session at provision; re-key it when Interchange anchors the run * Fix CI: prettier formatting, principal enum value, and workflowRunLaunchSpec fakes * Fix merge fallout: restore CL-7481 agent-session exports and drop a duplicate key Merging cl-7107's repin/re-vendor audit into this branch left packages/workflows/src/index.ts re-exporting the pre-CL-7481 agent-session API (recordAgentSessionForRun) that no longer exists, its matching test file testing that same stale API, and a duplicated eventCollectors key in apps/hub/src/index.ts's webhook-trigger launch wiring. Restores the current exports (recordAgentSessionAtProvision, ensureRunSession) and their test, and drops the duplicate object key. * Add tests for wake retry re-resolving the live address (CL-7486) sendRunMailWithReclaimRetry retried every attempt against the address it started with. Proves the reproduction of the production failure: a participant already one relaunch generation in that dies again mid-retry must have its next retry chase the run that replaced it, not keep hammering the address that just died. * Chat: wake retry chases the run a relaunch replaced it with (CL-7486) sendRunMailWithReclaimRetry retried every delivery attempt, and woke, against the address it captured at the top of the loop. A wake in between retries can relaunch the run (its dead-run branch repoints the room's stable participant at a fresh address without telling this loop), and a participant already one relaunch generation in that dies again mid-retry leaves this loop holding an address that is no longer either the stable participant id or the current run id — invisible to readBindingByAddressAnyTenant. The next wake for it then threw "No workbench_launch binding" instead of finding the run that replaced it, which is what chat e2e's mention-fanout and invite-drives-a-turn cases were hitting. Re-resolves the live address (and its session) off the stable participant id after each wake, so the retry loop always chases whichever run is actually current. Also hardens publishCorbitsToolsRegistry's create-then-list-on-conflict ensure: two overlapping seed runs racing the same tenant's package registry asset can still throw a raw asset_tenant_kind_name violation if the substrate's own 409 mapping is bypassed; the ensure now also recognizes that violation by name and resolves the winner's row instead of throwing. * Fix webhook-triggers launch test fixture: eventCollectors needs has() * Add test coverage for routable-wait relaunch chasing in sendRunMailWithReclaimRetry Drives the CL-7486 reclaim-retry test off routing-table transitions instead of fixed backoff timing, and updates the CL-7214 coalescing test's fake sidecar router to model a redeploy actually registering. * Chat: wait for the routing table instead of a fixed backoff before giving up on first-turn delivery sendRunMailWithReclaimRetry used to exhaust a ~7.75s backoff and report the invited agent unreachable while its sidecar was still booting and registering with the hub (CL-7476) -- fast enough to make the fan-out relaunch a perfectly healthy run. It now shares deliverWhenRoutable with the webhook trigger's ingress delivery: wake once on the first unreachable failure (still chasing a mid-wait relaunch by re-resolving the live address inside the retry's send closure), then wait on the hub's routing table up to the same boot-and-register budget (DEFAULT_ROUTABLE_DEADLINE_MS, now exported from @corbits/workflows) before retrying exactly once more. * Fix formatting * Add test coverage for ensureRunSession creating a session for an API-deployed run * ensureRunSession: create the session row instead of throwing when it's missing Runs deployed straight through Interchange's own deployments route never pass through a Workbench launcher, so recordAgentSessionAtProvision never runs for them and no agent_session row exists for the run. By the time persist/dispatch call ensureRunSession the run is anchored with a principal, so the seam has everything needed to create the row itself instead of treating the missing row as a bug.
* Add test for workflowDeployBody's catalog-offering shape
* e2e: deploy workflows by catalog offering ids
The native workflow deploy route now requires sourceOfferingIds/
defaultSourceOfferingId resolved against the tenant's own catalog,
rather than a raw provider/baseURL/apiKey source chain. Update
workflowDeployBody accordingly, and extract the zero-cost noop
catalog chain smoke-webhook.test.ts already built (an anthropic-
plugin provider pointed at the hub's noop-inference endpoint) into
a shared seedNoopCatalogOffering harness helper.
Every deploying e2e suite now seeds or resolves a real catalog
offering before deploying: walking-skeleton keeps a never-real
placeholder host (assertNeverRealProvider still guards it), chat
resolves the offering seedCatalog already planted rather than
seeding a second chain, and heartbeat/workbench-digest/smoke-webhook
seed the noop chain via the shared helper. cl-6324-launch-proof's
section-mode deploy now reuses the offering id the proof already
resolved when it narrowed the bench catalog to its pinned model.
* Fix formatting
* Add tests for deploying default workflows by catalog offering ids
seedTenant currently deploys every default workflow with a bare
provider/baseURL/apiKey source, but the hub's deploy route now requires
sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's
catalog. Update the seeding, onboarding, and hub config test suites to
expect the new offering-id based deploy body and provider/model-only
ModelSource shape ahead of the implementation change.
* Seeding: deploy default workflows by catalog offering ids
ensureDeployment posted a bare {provider, baseURL, apiKey, model}
source, but the hub's deploy route requires sourceOfferingIds and a
defaultSourceOfferingId resolved from the tenant's catalog, so seeding
a connect flow's "assistant" workflow 400'd. seedTenant now resolves
the tenant's real catalog offerings (sorted by priority, lowest wins
the default) for a normal workflow, and plants a dedicated noop
catalog offering for a modelSource-pinned one (heartbeat), pointed at
the hub's own noop-inference endpoint. ModelSource shrinks to a bare
provider/model pair — the baseURL/apiKey it used to carry only mattered
for the old deploy body, and callers that still need a key for seeding
a catalog (system-seed's root tenant, config's env-key auto-plant) keep
it on their own local type instead.
* longevity-sim: deploy workflows by catalog offering ids
Another workflowDeployBody caller CI's typecheck job caught: this
stack seeds one catalog offering per real inference target already,
so track each one's id and pass it through to the deploy body
instead of the raw provider/baseURL/apiKey shape.
* Fix formatting: prettier
* sandbox-sidecar: add tests for generation-0 ensure/destroy requests
The allocation service's initial sidecar_allocation and workflow_probe
rows start at generation 0 (@intx/db enforces generation >= 0, not >
0), so the first ensure() for a freshly allocated sidecar must be
accepted at generation 0. These tests cover that acceptance, later
generations, non-integer/negative rejection, and that stale-generation
fencing still rejects an older generation once a newer one has been
observed.
* sandbox-sidecar: accept the allocation service's initial generation
@intx/db's sidecar_allocation and workflow_probe tables both check
generation >= 0 and the allocation service records the first row for
an allocation at generation 0, but validateEnsureRequest() and
validateDestroyRequest() in packages/sandbox-sidecar rejected anything
that wasn't strictly positive, and the on-disk fencing schema in
state-store.ts required generation > 0. The first ensure() for any
newly allocated sidecar was rejected with 'generation must be a
positive integer', failing every deploy.
Accept generation >= 0 in both validators and the state-store schema;
stale-generation fencing in observeEnsure()/observeDestroy() already
compares against the stored generation rather than assuming positivity,
so it is unaffected.
* sandbox-sidecar: fix formatting in provisioner test
* Add tests for resolved catalog offerings and modelSourceFor's trimmed signature
Covers the new GET .../catalog/resolved-offerings route (denied grant,
and both directly-owned and ancestor-inherited offerings sorted by
priority) and updates seeding/onboarding test fixtures to the route's
response shape ahead of the implementation change.
* Seed deploys against a tenant's resolved catalog offerings, not just owned ones
resolveRealSourceOfferingIds listed only offerings a tenant owns
directly, missing anything inherited from an ancestor tenant — the
hub's own workflow deployer already resolves that wider,
ancestor-inheriting view via listVisibleOfferings. Adds
GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from
apps/hub, implemented in @corbits/inference-catalog so the deployer and
this route share one source of truth) and points seed at it instead.
Also drops the two long-dead unused parameters from
onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from
@corbits/seeding for the e2e harness to reuse.
* Add a regression test pinning the shared-sidecar auth gap (CL-7472)
At the interchange pin this branch stacks on (692c3106), the hub
authenticates a sidecar's register/reconnect frame only against a
sidecar row that resolves to a live sidecar_allocation (or
workflow_probe) row. Upstream dropped the older "shared" credential
scope this repo's e2e harness used to provision a single, standalone
sidecar identity for, so a provisionSidecar-only row now connects and
is rejected with "invalid token" in a loop rather than ever dialing
in. This test pins that negative so a future re-vendor that silently
restores (or relies on) the old scope surfaces here first.
* e2e: stop pre-provisioning a shared sidecar identity (CL-7472)
Every e2e suite dial-in relied on a single, harness-started sidecar
process registered against a bare `provisionSidecar` identity row.
At pin 692c3106, the hub only accepts a register/reconnect frame from
a sidecar whose identity resolves to a live sidecar_allocation (or
workflow_probe) row; a standalone identity with neither now fails the
handshake and gets rejected with "invalid token" every ~3s, forever
(chat mint 500s, deploys never dial in — CI run 34042907014).
The hub's own process-provisioner already spawns a dedicated,
correctly-allocated sidecar process per deployment on demand — the
same path production already takes for a real workflow run or a
minted agent chat. Every affected suite (walking skeleton, chat,
heartbeat, workbench-digest, smoke-chat, smoke-webhook,
greeting-delivery, channel-reconnect, local-rip, and the isolation
suite) now lets that happen instead of dialing in its own sidecar:
the existing 502/500 retry-until-ready loops already cover the wait,
and diagnostics that used to read the one sidecar process's output
now read the hub's — the process-provisioner backend spawns each
sidecar with its stdio inherited from the hub, so the hub's own
captured output already carries every spawned sidecar's log lines.
`provisionSidecar`/`startSidecar` stay in the harness (documented as
non-authenticating at this pin) for the handful of standalone proof
scripts not yet migrated off a directly-controlled sidecar process.
* ci: upload hub/sidecar e2e logs as a build artifact (CL-7472)
The e2e job's hub/sidecar output was never captured, so the CI run
that surfaced the sidecar disconnect loop (34042907014) had no
sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a
runner-temp directory and upload it on every run, pass or fail.
* Add tests for tenant-scoped workbench_launch address resolution (CL-7474)
* Chat: workbench_launch address/stableId lookups are tenant-scoped
readBindingByAddress and resolveLiveByStableId took an address or
stable id and resolved whichever workbench_launch row matched it,
regardless of which tenant that row belonged to. Every caller that
already knows its own tenant (sendMail's requireLive, and
refreshAgentInstanceFromDefinition) now passes it through and gets
undefined instead of another tenant's launch if the two ever
disagree. The one legitimate tenant-agnostic caller — the event
stream handler discovering which tenant an inbound address even
belongs to before it has one to check against — keeps working by
simply omitting the new optional parameter.
Also fixes offeringDigest: it joined catalog offering ids with a
NUL byte, which is not valid UTF-8 to Postgres the moment a tenant's
catalog carries more than one visible offering ("invalid byte
sequence for encoding \"UTF8\": 0x00"), permanently failing the
workbench_launch insert for every such launch. This is what was
failing 4 of scripts/e2e/chat.test.ts's cases under per-allocation
sidecars, where seedCatalog now produces more than one offering.
* Chat: run prettier on the tenant-scoping change
* Chat: tenant-scoped binding reads require the tenant; the one unscoped read is named
* Add tests for the shared retry-until-routable delivery helper
* Deliver a webhook trigger's opening mail once its sidecar becomes routable (CL-7476)
* Log a skip, not an error, when root tenant seed has no operator key configured
* Run prettier
* Chat: run prettier on the tenant-scoping test file
* Add a DB-gated test for recording a run's agent_session (CL-7477)
resolveRunSessionId (Interchange's mail-routing lookup) reads agent_session
keyed by a run's principal, but nothing writes that table for a native
launch since folded-runs was deleted -- a run's first turn silently fails
to persist mail. Proves recordAgentSessionForRun writes a row
resolveRunSessionId reads back, and updates the webhook launch fake DB to
model workflow_run/agent_session the way the fix now touches them.
* Native launch records the run's agent_session so mail and spans persist (CL-7477)
Before #632 deleted folded-runs, mintFoldedRun was the only writer of
agent_session, the table resolveRunSessionId (Interchange's mail-routing
lookup, vendor/intx/hub-sessions/src/hub-session-lookups.ts) reads by a
run's principal to find its session. Every current native launch path
mints its own sessionId and hands it to prepareProvisionedDeployment, but
nothing ever wrote it into agent_session -- a run's first turn logged
"Endpoint ... has no session for address" and silently dropped its mail.
workflow_run.principal_id is still null the instant
prepareProvisionedDeployment returns: a provisioned anchor is born
"deployed" with no principal, and only the run's first trigger reconciles
one onto it (Interchange's anchorWithPrincipal), through the exact same
trigger path every sendUserMessage drives. So the write has to land after
that first send, not right after provisioning.
@corbits/workflows now exports recordAgentSessionForRun, a shared helper
that reads a run's tenant/definition/principal fresh off workflow_run and
inserts the session row (idempotent, since every send after the first is a
no-op). Every call site that sends a message wires it in right after that
send: webhook-triggers' launch, agent-directory's one-shot prompt, and
chat's sendRunMail (the one chokepoint every chat send -- invite, wake,
relaunch -- funnels through, since an invite alone provisions a run with
no message and would only ever see a still-null principal). apps/hub's
agent-authored-workflow deployer mints a run with no message of its own
and its first trigger runs entirely through Interchange's native route
with no Workbench hook to record afterward, so it's left alone.
Session teardown reuses the same read: chat's relaunch marks the outgoing
run's session ended before repointing the room, and the one-shot prompt's
teardown ends its own session once its single turn settles.
* Add tests for the run's event collector create/abandon wiring
* Native launch creates the run's event collector so turns are recorded (CL-7479)
* Add tests for lazy, idempotent run-session ensure (CL-7480)
* Hub ensures a run's session and collector on first use (CL-7480)
Replaces the post-send recordAgentSessionForRun (CL-7477/CL-7479) with
one idempotent ensureRunSession, called lazily at the hub seams that
actually see a run become mail-routable: the mailbox persist wrapper
and the event-collector dispatch path, plus chat's pre-send mail
dispatch. Pre-creating the run's principal to dodge the timing isn't
an option -- Interchange's grant materialization treats a conflict on
that insert as grants already committed and throws.
* Fix prettier formatting in agent-session.ts (CL-7480)
* Add tests for eager session recording and launch-spec session lookup
* Record a run's session at provision; re-key it when Interchange anchors the run
* Fix CI: prettier formatting, principal enum value, and workflowRunLaunchSpec fakes
* Fix merge fallout: restore CL-7481 agent-session exports and drop a duplicate key
Merging cl-7107's repin/re-vendor audit into this branch left
packages/workflows/src/index.ts re-exporting the pre-CL-7481 agent-session
API (recordAgentSessionForRun) that no longer exists, its matching test
file testing that same stale API, and a duplicated eventCollectors key in
apps/hub/src/index.ts's webhook-trigger launch wiring. Restores the
current exports (recordAgentSessionAtProvision, ensureRunSession) and
their test, and drops the duplicate object key.
* Add tests for wake retry re-resolving the live address (CL-7486)
sendRunMailWithReclaimRetry retried every attempt against the address it
started with. Proves the reproduction of the production failure: a
participant already one relaunch generation in that dies again mid-retry
must have its next retry chase the run that replaced it, not keep
hammering the address that just died.
* Chat: wake retry chases the run a relaunch replaced it with (CL-7486)
sendRunMailWithReclaimRetry retried every delivery attempt, and woke,
against the address it captured at the top of the loop. A wake in
between retries can relaunch the run (its dead-run branch repoints the
room's stable participant at a fresh address without telling this loop),
and a participant already one relaunch generation in that dies again
mid-retry leaves this loop holding an address that is no longer either
the stable participant id or the current run id — invisible to
readBindingByAddressAnyTenant. The next wake for it then threw "No
workbench_launch binding" instead of finding the run that replaced it,
which is what chat e2e's mention-fanout and invite-drives-a-turn cases
were hitting.
Re-resolves the live address (and its session) off the stable
participant id after each wake, so the retry loop always chases whichever
run is actually current.
Also hardens publishCorbitsToolsRegistry's create-then-list-on-conflict
ensure: two overlapping seed runs racing the same tenant's package
registry asset can still throw a raw asset_tenant_kind_name violation if
the substrate's own 409 mapping is bypassed; the ensure now also
recognizes that violation by name and resolves the winner's row instead
of throwing.
* Fix webhook-triggers launch test fixture: eventCollectors needs has()
* Add test coverage for routable-wait relaunch chasing in sendRunMailWithReclaimRetry
Drives the CL-7486 reclaim-retry test off routing-table transitions
instead of fixed backoff timing, and updates the CL-7214 coalescing
test's fake sidecar router to model a redeploy actually registering.
* Chat: wait for the routing table instead of a fixed backoff before giving up on first-turn delivery
sendRunMailWithReclaimRetry used to exhaust a ~7.75s backoff and report
the invited agent unreachable while its sidecar was still booting and
registering with the hub (CL-7476) -- fast enough to make the fan-out
relaunch a perfectly healthy run. It now shares deliverWhenRoutable
with the webhook trigger's ingress delivery: wake once on the first
unreachable failure (still chasing a mid-wait relaunch by re-resolving
the live address inside the retry's send closure), then wait on the
hub's routing table up to the same boot-and-register budget
(DEFAULT_ROUTABLE_DEADLINE_MS, now exported from @corbits/workflows)
before retrying exactly once more.
* Fix formatting
* Add test coverage for ensureRunSession creating a session for an API-deployed run
* ensureRunSession: create the session row instead of throwing when it's missing
Runs deployed straight through Interchange's own deployments route
never pass through a Workbench launcher, so recordAgentSessionAtProvision
never runs for them and no agent_session row exists for the run. By the
time persist/dispatch call ensureRunSession the run is anchored with a
principal, so the seam has everything needed to create the row itself
instead of treating the missing row as a bug.
* Add tests for waiting on a booting run instead of relaunching it
isBeyondWake's completed-run test assumed status alone marks a run
terminal; a deployment settled "completed" before its first trigger is
still booting, not dead, so the predicate now needs a run that took a
turn to treat "completed" as terminal. Update platform-adapter's
lifecycle/ensureAwake/sendMail coverage to match: a live run that is
merely not yet routable is waited on rather than redeployed, so those
tests now exercise a genuinely terminal run instead of an offline one.
* Chat: a booting run is waited on, not relaunched
A provisioned anchor stays "deployed" until its first trigger, so a run
mid-boot is indistinguishable from an offline one by routability alone.
wakeByAddress's fallback branch treated the two the same and redeployed
either — minting a fresh run and address out from under the sidecar's
own in-flight registration, which supersedes its allocation and restarts
the boot. Repeated on every wake attempt, this produced an endless
reconnect loop with no run ever finishing its boot.
wakeByAddress now only relaunches a run whose status is genuinely
terminal; a live run that is not yet routable is left alone; the
caller's own routable wait (deliverWhenRoutable, via
sendRunMailWithReclaimRetry) is what actually resolves it once the
sidecar finishes registering. isBeyondWake also no longer treats every
"completed" status as terminal outright: a deployment settled complete
before its first trigger ever landed is still indistinguishable from one
mid-boot, so it is only terminal once the store can tell it took a turn
(a principal was ever anchored to it).
…terchange-main-before-the
…490) (#680) * Add test for workflowDeployBody's catalog-offering shape * e2e: deploy workflows by catalog offering ids The native workflow deploy route now requires sourceOfferingIds/ defaultSourceOfferingId resolved against the tenant's own catalog, rather than a raw provider/baseURL/apiKey source chain. Update workflowDeployBody accordingly, and extract the zero-cost noop catalog chain smoke-webhook.test.ts already built (an anthropic- plugin provider pointed at the hub's noop-inference endpoint) into a shared seedNoopCatalogOffering harness helper. Every deploying e2e suite now seeds or resolves a real catalog offering before deploying: walking-skeleton keeps a never-real placeholder host (assertNeverRealProvider still guards it), chat resolves the offering seedCatalog already planted rather than seeding a second chain, and heartbeat/workbench-digest/smoke-webhook seed the noop chain via the shared helper. cl-6324-launch-proof's section-mode deploy now reuses the offering id the proof already resolved when it narrowed the bench catalog to its pinned model. * Fix formatting * Add tests for deploying default workflows by catalog offering ids seedTenant currently deploys every default workflow with a bare provider/baseURL/apiKey source, but the hub's deploy route now requires sourceOfferingIds/defaultSourceOfferingId resolved from the tenant's catalog. Update the seeding, onboarding, and hub config test suites to expect the new offering-id based deploy body and provider/model-only ModelSource shape ahead of the implementation change. * Seeding: deploy default workflows by catalog offering ids ensureDeployment posted a bare {provider, baseURL, apiKey, model} source, but the hub's deploy route requires sourceOfferingIds and a defaultSourceOfferingId resolved from the tenant's catalog, so seeding a connect flow's "assistant" workflow 400'd. seedTenant now resolves the tenant's real catalog offerings (sorted by priority, lowest wins the default) for a normal workflow, and plants a dedicated noop catalog offering for a modelSource-pinned one (heartbeat), pointed at the hub's own noop-inference endpoint. ModelSource shrinks to a bare provider/model pair — the baseURL/apiKey it used to carry only mattered for the old deploy body, and callers that still need a key for seeding a catalog (system-seed's root tenant, config's env-key auto-plant) keep it on their own local type instead. * longevity-sim: deploy workflows by catalog offering ids Another workflowDeployBody caller CI's typecheck job caught: this stack seeds one catalog offering per real inference target already, so track each one's id and pass it through to the deploy body instead of the raw provider/baseURL/apiKey shape. * Fix formatting: prettier * sandbox-sidecar: add tests for generation-0 ensure/destroy requests The allocation service's initial sidecar_allocation and workflow_probe rows start at generation 0 (@intx/db enforces generation >= 0, not > 0), so the first ensure() for a freshly allocated sidecar must be accepted at generation 0. These tests cover that acceptance, later generations, non-integer/negative rejection, and that stale-generation fencing still rejects an older generation once a newer one has been observed. * sandbox-sidecar: accept the allocation service's initial generation @intx/db's sidecar_allocation and workflow_probe tables both check generation >= 0 and the allocation service records the first row for an allocation at generation 0, but validateEnsureRequest() and validateDestroyRequest() in packages/sandbox-sidecar rejected anything that wasn't strictly positive, and the on-disk fencing schema in state-store.ts required generation > 0. The first ensure() for any newly allocated sidecar was rejected with 'generation must be a positive integer', failing every deploy. Accept generation >= 0 in both validators and the state-store schema; stale-generation fencing in observeEnsure()/observeDestroy() already compares against the stored generation rather than assuming positivity, so it is unaffected. * sandbox-sidecar: fix formatting in provisioner test * Add tests for resolved catalog offerings and modelSourceFor's trimmed signature Covers the new GET .../catalog/resolved-offerings route (denied grant, and both directly-owned and ancestor-inherited offerings sorted by priority) and updates seeding/onboarding test fixtures to the route's response shape ahead of the implementation change. * Seed deploys against a tenant's resolved catalog offerings, not just owned ones resolveRealSourceOfferingIds listed only offerings a tenant owns directly, missing anything inherited from an ancestor tenant — the hub's own workflow deployer already resolves that wider, ancestor-inheriting view via listVisibleOfferings. Adds GET /api/tenants/:tenantId/catalog/resolved-offerings (mounted from apps/hub, implemented in @corbits/inference-catalog so the deployer and this route share one source of truth) and points seed at it instead. Also drops the two long-dead unused parameters from onboarding's modelSourceFor, and exports ensureNoopCatalogOffering from @corbits/seeding for the e2e harness to reuse. * Add a regression test pinning the shared-sidecar auth gap (CL-7472) At the interchange pin this branch stacks on (692c3106), the hub authenticates a sidecar's register/reconnect frame only against a sidecar row that resolves to a live sidecar_allocation (or workflow_probe) row. Upstream dropped the older "shared" credential scope this repo's e2e harness used to provision a single, standalone sidecar identity for, so a provisionSidecar-only row now connects and is rejected with "invalid token" in a loop rather than ever dialing in. This test pins that negative so a future re-vendor that silently restores (or relies on) the old scope surfaces here first. * e2e: stop pre-provisioning a shared sidecar identity (CL-7472) Every e2e suite dial-in relied on a single, harness-started sidecar process registered against a bare `provisionSidecar` identity row. At pin 692c3106, the hub only accepts a register/reconnect frame from a sidecar whose identity resolves to a live sidecar_allocation (or workflow_probe) row; a standalone identity with neither now fails the handshake and gets rejected with "invalid token" every ~3s, forever (chat mint 500s, deploys never dial in — CI run 34042907014). The hub's own process-provisioner already spawns a dedicated, correctly-allocated sidecar process per deployment on demand — the same path production already takes for a real workflow run or a minted agent chat. Every affected suite (walking skeleton, chat, heartbeat, workbench-digest, smoke-chat, smoke-webhook, greeting-delivery, channel-reconnect, local-rip, and the isolation suite) now lets that happen instead of dialing in its own sidecar: the existing 502/500 retry-until-ready loops already cover the wait, and diagnostics that used to read the one sidecar process's output now read the hub's — the process-provisioner backend spawns each sidecar with its stdio inherited from the hub, so the hub's own captured output already carries every spawned sidecar's log lines. `provisionSidecar`/`startSidecar` stay in the harness (documented as non-authenticating at this pin) for the handful of standalone proof scripts not yet migrated off a directly-controlled sidecar process. * ci: upload hub/sidecar e2e logs as a build artifact (CL-7472) The e2e job's hub/sidecar output was never captured, so the CI run that surfaced the sidecar disconnect loop (34042907014) had no sidecar-side log to read past the transcript. Point E2E_LOG_DIR at a runner-temp directory and upload it on every run, pass or fail. * Add tests for tenant-scoped workbench_launch address resolution (CL-7474) * Chat: workbench_launch address/stableId lookups are tenant-scoped readBindingByAddress and resolveLiveByStableId took an address or stable id and resolved whichever workbench_launch row matched it, regardless of which tenant that row belonged to. Every caller that already knows its own tenant (sendMail's requireLive, and refreshAgentInstanceFromDefinition) now passes it through and gets undefined instead of another tenant's launch if the two ever disagree. The one legitimate tenant-agnostic caller — the event stream handler discovering which tenant an inbound address even belongs to before it has one to check against — keeps working by simply omitting the new optional parameter. Also fixes offeringDigest: it joined catalog offering ids with a NUL byte, which is not valid UTF-8 to Postgres the moment a tenant's catalog carries more than one visible offering ("invalid byte sequence for encoding \"UTF8\": 0x00"), permanently failing the workbench_launch insert for every such launch. This is what was failing 4 of scripts/e2e/chat.test.ts's cases under per-allocation sidecars, where seedCatalog now produces more than one offering. * Chat: run prettier on the tenant-scoping change * Chat: tenant-scoped binding reads require the tenant; the one unscoped read is named * Add tests for the shared retry-until-routable delivery helper * Deliver a webhook trigger's opening mail once its sidecar becomes routable (CL-7476) * Log a skip, not an error, when root tenant seed has no operator key configured * Run prettier * Chat: run prettier on the tenant-scoping test file * Add a DB-gated test for recording a run's agent_session (CL-7477) resolveRunSessionId (Interchange's mail-routing lookup) reads agent_session keyed by a run's principal, but nothing writes that table for a native launch since folded-runs was deleted -- a run's first turn silently fails to persist mail. Proves recordAgentSessionForRun writes a row resolveRunSessionId reads back, and updates the webhook launch fake DB to model workflow_run/agent_session the way the fix now touches them. * Native launch records the run's agent_session so mail and spans persist (CL-7477) Before #632 deleted folded-runs, mintFoldedRun was the only writer of agent_session, the table resolveRunSessionId (Interchange's mail-routing lookup, vendor/intx/hub-sessions/src/hub-session-lookups.ts) reads by a run's principal to find its session. Every current native launch path mints its own sessionId and hands it to prepareProvisionedDeployment, but nothing ever wrote it into agent_session -- a run's first turn logged "Endpoint ... has no session for address" and silently dropped its mail. workflow_run.principal_id is still null the instant prepareProvisionedDeployment returns: a provisioned anchor is born "deployed" with no principal, and only the run's first trigger reconciles one onto it (Interchange's anchorWithPrincipal), through the exact same trigger path every sendUserMessage drives. So the write has to land after that first send, not right after provisioning. @corbits/workflows now exports recordAgentSessionForRun, a shared helper that reads a run's tenant/definition/principal fresh off workflow_run and inserts the session row (idempotent, since every send after the first is a no-op). Every call site that sends a message wires it in right after that send: webhook-triggers' launch, agent-directory's one-shot prompt, and chat's sendRunMail (the one chokepoint every chat send -- invite, wake, relaunch -- funnels through, since an invite alone provisions a run with no message and would only ever see a still-null principal). apps/hub's agent-authored-workflow deployer mints a run with no message of its own and its first trigger runs entirely through Interchange's native route with no Workbench hook to record afterward, so it's left alone. Session teardown reuses the same read: chat's relaunch marks the outgoing run's session ended before repointing the room, and the one-shot prompt's teardown ends its own session once its single turn settles. * Add tests for the run's event collector create/abandon wiring * Native launch creates the run's event collector so turns are recorded (CL-7479) * Add tests for lazy, idempotent run-session ensure (CL-7480) * Hub ensures a run's session and collector on first use (CL-7480) Replaces the post-send recordAgentSessionForRun (CL-7477/CL-7479) with one idempotent ensureRunSession, called lazily at the hub seams that actually see a run become mail-routable: the mailbox persist wrapper and the event-collector dispatch path, plus chat's pre-send mail dispatch. Pre-creating the run's principal to dodge the timing isn't an option -- Interchange's grant materialization treats a conflict on that insert as grants already committed and throws. * Fix prettier formatting in agent-session.ts (CL-7480) * Add tests for eager session recording and launch-spec session lookup * Record a run's session at provision; re-key it when Interchange anchors the run * Fix CI: prettier formatting, principal enum value, and workflowRunLaunchSpec fakes * Fix merge fallout: restore CL-7481 agent-session exports and drop a duplicate key Merging cl-7107's repin/re-vendor audit into this branch left packages/workflows/src/index.ts re-exporting the pre-CL-7481 agent-session API (recordAgentSessionForRun) that no longer exists, its matching test file testing that same stale API, and a duplicated eventCollectors key in apps/hub/src/index.ts's webhook-trigger launch wiring. Restores the current exports (recordAgentSessionAtProvision, ensureRunSession) and their test, and drops the duplicate object key. * Add tests for wake retry re-resolving the live address (CL-7486) sendRunMailWithReclaimRetry retried every attempt against the address it started with. Proves the reproduction of the production failure: a participant already one relaunch generation in that dies again mid-retry must have its next retry chase the run that replaced it, not keep hammering the address that just died. * Chat: wake retry chases the run a relaunch replaced it with (CL-7486) sendRunMailWithReclaimRetry retried every delivery attempt, and woke, against the address it captured at the top of the loop. A wake in between retries can relaunch the run (its dead-run branch repoints the room's stable participant at a fresh address without telling this loop), and a participant already one relaunch generation in that dies again mid-retry leaves this loop holding an address that is no longer either the stable participant id or the current run id — invisible to readBindingByAddressAnyTenant. The next wake for it then threw "No workbench_launch binding" instead of finding the run that replaced it, which is what chat e2e's mention-fanout and invite-drives-a-turn cases were hitting. Re-resolves the live address (and its session) off the stable participant id after each wake, so the retry loop always chases whichever run is actually current. Also hardens publishCorbitsToolsRegistry's create-then-list-on-conflict ensure: two overlapping seed runs racing the same tenant's package registry asset can still throw a raw asset_tenant_kind_name violation if the substrate's own 409 mapping is bypassed; the ensure now also recognizes that violation by name and resolves the winner's row instead of throwing. * Fix webhook-triggers launch test fixture: eventCollectors needs has() * Add test coverage for routable-wait relaunch chasing in sendRunMailWithReclaimRetry Drives the CL-7486 reclaim-retry test off routing-table transitions instead of fixed backoff timing, and updates the CL-7214 coalescing test's fake sidecar router to model a redeploy actually registering. * Chat: wait for the routing table instead of a fixed backoff before giving up on first-turn delivery sendRunMailWithReclaimRetry used to exhaust a ~7.75s backoff and report the invited agent unreachable while its sidecar was still booting and registering with the hub (CL-7476) -- fast enough to make the fan-out relaunch a perfectly healthy run. It now shares deliverWhenRoutable with the webhook trigger's ingress delivery: wake once on the first unreachable failure (still chasing a mid-wait relaunch by re-resolving the live address inside the retry's send closure), then wait on the hub's routing table up to the same boot-and-register budget (DEFAULT_ROUTABLE_DEADLINE_MS, now exported from @corbits/workflows) before retrying exactly once more. * Fix formatting * Add test coverage for ensureRunSession creating a session for an API-deployed run * ensureRunSession: create the session row instead of throwing when it's missing Runs deployed straight through Interchange's own deployments route never pass through a Workbench launcher, so recordAgentSessionAtProvision never runs for them and no agent_session row exists for the run. By the time persist/dispatch call ensureRunSession the run is anchored with a principal, so the seam has everything needed to create the row itself instead of treating the missing row as a bug. * Add tests for waiting on a booting run instead of relaunching it isBeyondWake's completed-run test assumed status alone marks a run terminal; a deployment settled "completed" before its first trigger is still booting, not dead, so the predicate now needs a run that took a turn to treat "completed" as terminal. Update platform-adapter's lifecycle/ensureAwake/sendMail coverage to match: a live run that is merely not yet routable is waited on rather than redeployed, so those tests now exercise a genuinely terminal run instead of an offline one. * Chat: a booting run is waited on, not relaunched A provisioned anchor stays "deployed" until its first trigger, so a run mid-boot is indistinguishable from an offline one by routability alone. wakeByAddress's fallback branch treated the two the same and redeployed either — minting a fresh run and address out from under the sidecar's own in-flight registration, which supersedes its allocation and restarts the boot. Repeated on every wake attempt, this produced an endless reconnect loop with no run ever finishing its boot. wakeByAddress now only relaunches a run whose status is genuinely terminal; a live run that is not yet routable is left alone; the caller's own routable wait (deliverWhenRoutable, via sendRunMailWithReclaimRetry) is what actually resolves it once the sidecar finishes registering. isBeyondWake also no longer treats every "completed" status as terminal outright: a deployment settled complete before its first trigger ever landed is still indistinguishable from one mid-boot, so it is only terminal once the store can tell it took a turn (a principal was ever anchored to it). * Add tests for chat's run-trigger delivery (CL-7490) Retargets platform-adapter's fan-out assertions from a raw sessionService.sendUserMessage call to the new RunTriggerClient seam: the fake now records anchorRunId-keyed trigger calls and mints a messageId the same shape the real route does, so the reclaim-retry and threading assertions exercise the seam production code will actually use. * Chat delivers room mail through Interchange's run-trigger route (CL-7490) A provisioned run is deployed to its sidecar lazily by its first native trigger (vendor/intx/hub-api's createWorkflowRunTrigger: anchor -> grants/principal -> durable dispatch's agent.deploy -> deliver mail). Chat sent via sessionService.sendUserMessage, which delivers straight over the sidecar's ordinary channel and never reaches that durable-dispatch branch, so an invited run never triggered natively never got its first agent.deploy and stayed permanently unreachable. createWorkflowRunTrigger itself is not exported from @intx/hub-api (it backs the route, not a published seam), so chat now calls the route itself: a new RunTriggerClient (run-trigger-client.ts) posts to POST /api/tenants/:tenantId/workflows/runs/:runId/mail in-process against the hub's own composed Hono app. The route authenticates through @intx/hub-api's ordinary session contract, which chat cannot present a browser cookie for (most sends have no inbound HTTP request in scope at all), so the client signs a short-lived internal token naming the principal to authenticate as (run-trigger-internal-auth.ts); apps/hub's own getSession implementation -- a seam @intx/hub-api documents as pluggable -- verifies that token before falling through to the real cookie-session path. Neither side touches vendor/intx. send-run-mail.ts now delivers through the trigger client and uses its returned messageId for session_mail correlation, still assembling a local signed MIME copy (with @intx/mime/@intx/crypto directly) purely for this workbench's own history and attachment blob reads -- the trigger route returns no raw bytes, and nothing here delivers that local copy anywhere a second time. * Fix lint formatting and report-error finding in run-trigger-client Prettier formatting on index.ts/run-trigger-client.ts, and the report-error check's one real finding: readJson's parse-failure catch now carries a report-error-ignore note explaining that a malformed response body is still caught (loudly) by the caller's own arktype parse of the response.
…obe allocation (CL-7492)
Summary
Re-pin Interchange vendored trees to
692c3106(upstream main) and cut Workbench over to native provisioned deployments.Workbench is a client of Interchange. Invite, wake, webhook, and one-shot call
prepareProvisionedDeploymentonto the instance's existingkind:workflowasset at HEAD, thensendUserMessage. Chat records the returned run id, deployment address, and offering-id digest. No per-run source-tree render, no pre-minted anchor run, no chat-ownedagent_session.Turns are one native
sendUserMessageper addressed agent, carrying Message-ID / In-Reply-To / References. Launch is asynchronous: mail sent before ready is queued. Wake of a reaped instance is a freshprepareProvisionedDeploymentonto the same asset.Inference sources ride as catalog offering ids (
sourceOfferingIds/defaultSourceOfferingId). Tool package pins are passed natively.Deleted
@corbits/folded-runs@corbits/folded-run-one-shotdeployAdoptedWorkflowFromSource/deployWorkflowFromSource)Pin
Vendored
@intx/*at692c3106. Ledger kill dates 2026-11-03.Fixes CL-7107
Part of CL-6534
Part of CL-7445