Skip to content

Commit 03fb5fd

Browse files
Merge pull request #1424 from corbitsdev/cl-10651-intx-re-sync-vendor-snapshot-to-040-re-carry-local-patches
chore(vendor): re-sync interchange vendor trees to 0.4.0, re-carry patches
2 parents 5090c93 + 7814144 commit 03fb5fd

31 files changed

Lines changed: 935 additions & 202 deletions

‎docs/VENDORING.md‎

Lines changed: 149 additions & 43 deletions
Large diffs are not rendered by default.

‎vendor/intx-agent/PATCHES.md‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,17 @@ All three entries re-carried as-is; upstream has no `loadErrors`
1515
No adaptation needed — upstream did not touch these regions. Upstream's
1616
`credentialId` auth-model rewrite does not intersect these patches.
1717

18+
### 2026-10-14 re-sync (upstream `5453d0b0` — npm `0.4.0`, CL-10651)
19+
20+
All three entries re-carried unchanged: upstream did not touch
21+
`agent.ts` or `testing/audit-noop.ts` in this range, and `loadErrors` is
22+
still absent upstream. Upstream moved `waitForReactorDone` out of three
23+
test files into the new `testing/reactor-waiters.ts` (exported from
24+
`testing/index.ts`); `flush-errors.test.ts` and
25+
`audit-integration.test.ts` were three-way merged so the local
26+
`loadErrors` hooks and the FORBIDDEN_DEPS credential-failure companion
27+
suite re-carried onto the new shapes.
28+
1829
## agent-ts-resume-error-seq
1930

2031
`agent.ts` — `createAgent` resumes `errorSeq` from `auditStore.loadErrors`

‎vendor/intx-agent/src/agent.test.ts‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -289,9 +289,13 @@ describe("createAgent tool-rollback dispose handling", () => {
289289
if (!(caught instanceof Error)) throw new Error("unreachable");
290290
expect(caught.name).toBe("DuplicateToolError");
291291

292-
// Let any in-flight microtasks settle so an escaped rejection
293-
// would land.
294-
await new Promise((resolve) => setTimeout(resolve, 25));
292+
// One macrotask boundary is enough: the runtime reports an
293+
// unhandled rejection when the microtask queue drains, so a
294+
// rejection escaping the rollback above would already have
295+
// reached the handler by the time this resolves. Verified against
296+
// this runtime with a `void`-discarded rejection of the same
297+
// shape as the disposer's.
298+
await new Promise((resolve) => setTimeout(resolve, 0));
295299
expect(observedRejection).toBeNull();
296300
} finally {
297301
process.off("unhandledRejection", onUnhandled);

‎vendor/intx-agent/src/audit-integration.test.ts‎

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { defineDirector } from "./director";
4040
import { createDirectorRegistry } from "./director-registry";
4141
import type { BaseEnv } from "./env";
4242
import { defineTool } from "./tool";
43+
import { waitForReactorDone } from "./testing";
4344

4445
// The audit pipeline needs an inference source to construct the
4546
// reactor, but the test director never calls infer(); the URL is
@@ -206,14 +207,6 @@ function inboundConversation(): ReturnType<typeof createInboundMessage> {
206207
});
207208
}
208209

209-
async function waitForReactorDone(
210-
stream: AsyncIterable<{ type: string }>,
211-
): Promise<void> {
212-
for await (const event of stream) {
213-
if (event.type === "reactor.done") return;
214-
}
215-
}
216-
217210
describe("agent audit integration", () => {
218211
let workDir: string;
219212

‎vendor/intx-agent/src/compactor-wiring.test.ts‎

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import { createDirectorRegistry } from "./director-registry";
4343
import type { BaseEnv } from "./env";
4444
import { noopAuditStore } from "./testing/audit-noop";
4545
import { permissiveAuthorize } from "./testing/authorize-allow";
46+
import { waitForReactorDone } from "./testing";
4647

4748
// The compactor tests never call infer(); the URL is unreachable so any
4849
// accidental infer() would fail fast rather than hang.
@@ -187,14 +188,6 @@ function inboundConversation(content: string) {
187188
});
188189
}
189190

190-
async function waitForReactorDone(
191-
stream: AsyncIterable<ReactorEmittedEvent>,
192-
): Promise<void> {
193-
for await (const event of stream) {
194-
if (event.type === "reactor.done") return;
195-
}
196-
}
197-
198191
async function collectUntilDone(
199192
stream: AsyncIterable<ReactorEmittedEvent>,
200193
): Promise<ReactorEmittedEvent[]> {
@@ -203,7 +196,11 @@ async function collectUntilDone(
203196
events.push(event);
204197
if (event.type === "reactor.done") return events;
205198
}
206-
return events;
199+
// A fatal reactor.error still shuts the reactor down, so reactor.done
200+
// arrives on that path too and the early return above is what fires.
201+
// Reaching here means the stream closed without it, and returning the
202+
// partial events would let the caller assert against a truncated run.
203+
throw new Error("reactor event stream ended before reactor.done");
207204
}
208205

209206
describe("createAgent compactor wiring", () => {

‎vendor/intx-agent/src/flush-errors.test.ts‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import { defineDirector } from "./director";
3333
import { createDirectorRegistry } from "./director-registry";
3434
import type { BaseEnv } from "./env";
3535
import { permissiveAuthorize } from "./testing/authorize-allow";
36+
import { waitForReactorDone } from "./testing";
3637

3738
// An unreachable URL causes the inference call to fail with a network
3839
// error, which the reactor surfaces as an `inference.error` event.
@@ -212,18 +213,9 @@ function inboundConversation(): ReturnType<typeof createInboundMessage> {
212213
});
213214
}
214215

215-
// Drain agent.stream() until a `reactor.done` event is observed, then
216-
// resolve. The agent's terminal event signals the reactor has settled
217-
// and any pending audit-flush has had a chance to run via the
218-
// assembly's onShutdown hook.
219-
async function waitForReactorDone(
220-
stream: AsyncIterable<{ type: string }>,
221-
): Promise<void> {
222-
for await (const event of stream) {
223-
if (event.type === "reactor.done") return;
224-
}
225-
}
226-
216+
// Locally patched — see vendor/intx-agent/PATCHES.md#agent-ts-duplicate-error-flush
217+
// (the FORBIDDEN_DEPS credential-failure suite below is the companion test
218+
// block re-carried with the auth-recovery and duplicate-flush entries).
227219
const FORBIDDEN_DEPS = {
228220
...createDefaultDependencies(),
229221
fetch: async () =>

‎vendor/intx-agent/src/testing/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@
1616

1717
export { noopAuditStore } from "./audit-noop";
1818
export { permissiveAuthorize } from "./authorize-allow";
19+
export { waitForReactorDone } from "./reactor-waiters";
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, test, expect } from "bun:test";
2+
3+
import { waitForReactorDone } from "./reactor-waiters";
4+
5+
async function* stream(...events: { type: string }[]) {
6+
for (const event of events) yield event;
7+
}
8+
9+
describe("waitForReactorDone", () => {
10+
test("resolves on the terminal event", async () => {
11+
await waitForReactorDone(
12+
stream({ type: "reactor.start" }, { type: "reactor.done" }),
13+
);
14+
});
15+
16+
test("throws when the stream ends without the terminal event", async () => {
17+
// Returning instead would resolve as though the run had completed, and
18+
// the caller would then fail on a later assertion rather than on the
19+
// real cause. All four copies of this helper returned.
20+
await expect(
21+
waitForReactorDone(stream({ type: "reactor.start" })),
22+
).rejects.toThrow(/ended before reactor.done/);
23+
});
24+
25+
test("ignores events after the terminal one", async () => {
26+
await waitForReactorDone(
27+
stream({ type: "reactor.done" }, { type: "reactor.stray" }),
28+
);
29+
});
30+
});
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Waiter over a reactor's emitted-event stream.
2+
//
3+
// A reactor run ends with a `reactor.done` event, including on the fatal
4+
// error path, so a test that needs the run finished waits for that event
5+
// rather than for an interval. CONVENTIONS.md names this shape as the one to
6+
// copy; it lives here so it is imported rather than copied again.
7+
8+
/**
9+
* Resolve once the stream emits `reactor.done`.
10+
*
11+
* Throws if the stream ends without it: returning would resolve as though the
12+
* run had completed, and the caller would then fail on a later assertion
13+
* instead of on the real cause.
14+
*
15+
* Typed structurally so a caller need not import the reactor's event union to
16+
* await its terminal event.
17+
*/
18+
export async function waitForReactorDone(
19+
stream: AsyncIterable<{ type: string }>,
20+
): Promise<void> {
21+
for await (const event of stream) {
22+
if (event.type === "reactor.done") return;
23+
}
24+
throw new Error("reactor event stream ended before reactor.done");
25+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// How a wildcard grant row and a bare `tool:<name>` row each fare against a
2+
// PINNED tool's runtime gate.
3+
//
4+
// A pinned tool package is loaded through the namespacing loader, which
5+
// rewrites every definition name to `<bundleId>:<name>`. The workflow child
6+
// gates each call on `tool:<call.name>`, so the query it issues for a pinned
7+
// tool is `tool:<bundleId>:<name>`. The deploy-time capability walk, by
8+
// contrast, reads inline `agent.toolFactories`, which carry no bundle context,
9+
// so the rows it contributes are bare `tool:<name>`.
10+
//
11+
// That asymmetry is what makes the namespaced form a step-scoping mechanism:
12+
// a bare row cannot address a pinned tool's gate, and the authority a pinned
13+
// tool actually needs is supplied per step by the sidecar's tool-mark floor,
14+
// derived from the factories that step loaded. These cases pin what a wildcard
15+
// row does to that scoping.
16+
17+
import { describe, test, expect } from "bun:test";
18+
19+
import { evaluateGrants } from "./evaluate";
20+
import type { GrantRule } from "./types";
21+
22+
const BUNDLE_ID = "tp_pinned_bundle";
23+
const TOOL_NAME = "search";
24+
const NAMESPACED_GATE = `tool:${BUNDLE_ID}:${TOOL_NAME}`;
25+
const BARE_GATE = `tool:${TOOL_NAME}`;
26+
const INVOKE = "invoke";
27+
28+
function grant(
29+
overrides: Partial<GrantRule> &
30+
Pick<GrantRule, "resource" | "action" | "effect">,
31+
): GrantRule {
32+
return {
33+
id: `grt_${Math.random().toString(36).slice(2, 10)}`,
34+
origin: "creator",
35+
conditions: null,
36+
expiresAt: null,
37+
roleId: null,
38+
principalId: null,
39+
...overrides,
40+
};
41+
}
42+
43+
describe("a wildcard row reaches a pinned tool's namespaced gate", () => {
44+
test("*/* resolves to allow for tool:<bundleId>:<name>/invoke", async () => {
45+
const result = await evaluateGrants(
46+
[grant({ resource: "*", action: "*", effect: "allow" })],
47+
NAMESPACED_GATE,
48+
INVOKE,
49+
);
50+
expect(result.effect).toBe("allow");
51+
});
52+
53+
test("the walk's bare tool:<name> row does NOT reach that gate", async () => {
54+
const result = await evaluateGrants(
55+
[grant({ resource: BARE_GATE, action: INVOKE, effect: "allow" })],
56+
NAMESPACED_GATE,
57+
INVOKE,
58+
);
59+
expect(result.effect).toBe(null);
60+
});
61+
62+
test("the bare row is operative against a bare, unpinned gate", async () => {
63+
const result = await evaluateGrants(
64+
[grant({ resource: BARE_GATE, action: INVOKE, effect: "allow" })],
65+
BARE_GATE,
66+
INVOKE,
67+
);
68+
expect(result.effect).toBe("allow");
69+
});
70+
71+
test("a wildcard row reaches a second bundle's tool the step never loaded", async () => {
72+
const result = await evaluateGrants(
73+
[grant({ resource: "*", action: "*", effect: "allow" })],
74+
"tool:tp_other_bundle:transfer",
75+
INVOKE,
76+
);
77+
expect(result.effect).toBe("allow");
78+
});
79+
});
80+
81+
describe("the tool-mark floor still outranks a wildcard row", () => {
82+
test("an ask floor beats */* allow at higher specificity", async () => {
83+
const floor = grant({
84+
resource: NAMESPACED_GATE,
85+
action: INVOKE,
86+
effect: "ask",
87+
});
88+
const wildcard = grant({ resource: "*", action: "*", effect: "allow" });
89+
const result = await evaluateGrants(
90+
[wildcard, floor],
91+
NAMESPACED_GATE,
92+
INVOKE,
93+
);
94+
expect(result.effect).toBe("ask");
95+
});
96+
});

0 commit comments

Comments
 (0)