Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 149 additions & 43 deletions docs/VENDORING.md

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions vendor/intx-agent/PATCHES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@ All three entries re-carried as-is; upstream has no `loadErrors`
No adaptation needed — upstream did not touch these regions. Upstream's
`credentialId` auth-model rewrite does not intersect these patches.

### 2026-10-14 re-sync (upstream `5453d0b0` — npm `0.4.0`, CL-10651)

All three entries re-carried unchanged: upstream did not touch
`agent.ts` or `testing/audit-noop.ts` in this range, and `loadErrors` is
still absent upstream. Upstream moved `waitForReactorDone` out of three
test files into the new `testing/reactor-waiters.ts` (exported from
`testing/index.ts`); `flush-errors.test.ts` and
`audit-integration.test.ts` were three-way merged so the local
`loadErrors` hooks and the FORBIDDEN_DEPS credential-failure companion
suite re-carried onto the new shapes.

## agent-ts-resume-error-seq

`agent.ts` — `createAgent` resumes `errorSeq` from `auditStore.loadErrors`
Expand Down
10 changes: 7 additions & 3 deletions vendor/intx-agent/src/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,9 +289,13 @@ describe("createAgent tool-rollback dispose handling", () => {
if (!(caught instanceof Error)) throw new Error("unreachable");
expect(caught.name).toBe("DuplicateToolError");

// Let any in-flight microtasks settle so an escaped rejection
// would land.
await new Promise((resolve) => setTimeout(resolve, 25));
// One macrotask boundary is enough: the runtime reports an
// unhandled rejection when the microtask queue drains, so a
// rejection escaping the rollback above would already have
// reached the handler by the time this resolves. Verified against
// this runtime with a `void`-discarded rejection of the same
// shape as the disposer's.
await new Promise((resolve) => setTimeout(resolve, 0));
expect(observedRejection).toBeNull();
} finally {
process.off("unhandledRejection", onUnhandled);
Expand Down
9 changes: 1 addition & 8 deletions vendor/intx-agent/src/audit-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { defineDirector } from "./director";
import { createDirectorRegistry } from "./director-registry";
import type { BaseEnv } from "./env";
import { defineTool } from "./tool";
import { waitForReactorDone } from "./testing";

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

async function waitForReactorDone(
stream: AsyncIterable<{ type: string }>,
): Promise<void> {
for await (const event of stream) {
if (event.type === "reactor.done") return;
}
}

describe("agent audit integration", () => {
let workDir: string;

Expand Down
15 changes: 6 additions & 9 deletions vendor/intx-agent/src/compactor-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import { createDirectorRegistry } from "./director-registry";
import type { BaseEnv } from "./env";
import { noopAuditStore } from "./testing/audit-noop";
import { permissiveAuthorize } from "./testing/authorize-allow";
import { waitForReactorDone } from "./testing";

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

async function waitForReactorDone(
stream: AsyncIterable<ReactorEmittedEvent>,
): Promise<void> {
for await (const event of stream) {
if (event.type === "reactor.done") return;
}
}

async function collectUntilDone(
stream: AsyncIterable<ReactorEmittedEvent>,
): Promise<ReactorEmittedEvent[]> {
Expand All @@ -203,7 +196,11 @@ async function collectUntilDone(
events.push(event);
if (event.type === "reactor.done") return events;
}
return events;
// A fatal reactor.error still shuts the reactor down, so reactor.done
// arrives on that path too and the early return above is what fires.
// Reaching here means the stream closed without it, and returning the
// partial events would let the caller assert against a truncated run.
throw new Error("reactor event stream ended before reactor.done");
}

describe("createAgent compactor wiring", () => {
Expand Down
16 changes: 4 additions & 12 deletions vendor/intx-agent/src/flush-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import { defineDirector } from "./director";
import { createDirectorRegistry } from "./director-registry";
import type { BaseEnv } from "./env";
import { permissiveAuthorize } from "./testing/authorize-allow";
import { waitForReactorDone } from "./testing";

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

// Drain agent.stream() until a `reactor.done` event is observed, then
// resolve. The agent's terminal event signals the reactor has settled
// and any pending audit-flush has had a chance to run via the
// assembly's onShutdown hook.
async function waitForReactorDone(
stream: AsyncIterable<{ type: string }>,
): Promise<void> {
for await (const event of stream) {
if (event.type === "reactor.done") return;
}
}

// Locally patched — see vendor/intx-agent/PATCHES.md#agent-ts-duplicate-error-flush
// (the FORBIDDEN_DEPS credential-failure suite below is the companion test
// block re-carried with the auth-recovery and duplicate-flush entries).
const FORBIDDEN_DEPS = {
...createDefaultDependencies(),
fetch: async () =>
Expand Down
1 change: 1 addition & 0 deletions vendor/intx-agent/src/testing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@

export { noopAuditStore } from "./audit-noop";
export { permissiveAuthorize } from "./authorize-allow";
export { waitForReactorDone } from "./reactor-waiters";
30 changes: 30 additions & 0 deletions vendor/intx-agent/src/testing/reactor-waiters.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { describe, test, expect } from "bun:test";

import { waitForReactorDone } from "./reactor-waiters";

async function* stream(...events: { type: string }[]) {
for (const event of events) yield event;
}

describe("waitForReactorDone", () => {
test("resolves on the terminal event", async () => {
await waitForReactorDone(
stream({ type: "reactor.start" }, { type: "reactor.done" }),
);
});

test("throws when the stream ends without the terminal event", async () => {
// Returning instead would resolve as though the run had completed, and
// the caller would then fail on a later assertion rather than on the
// real cause. All four copies of this helper returned.
await expect(
waitForReactorDone(stream({ type: "reactor.start" })),
).rejects.toThrow(/ended before reactor.done/);
});

test("ignores events after the terminal one", async () => {
await waitForReactorDone(
stream({ type: "reactor.done" }, { type: "reactor.stray" }),
);
});
});
25 changes: 25 additions & 0 deletions vendor/intx-agent/src/testing/reactor-waiters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Waiter over a reactor's emitted-event stream.
//
// A reactor run ends with a `reactor.done` event, including on the fatal
// error path, so a test that needs the run finished waits for that event
// rather than for an interval. CONVENTIONS.md names this shape as the one to
// copy; it lives here so it is imported rather than copied again.

/**
* Resolve once the stream emits `reactor.done`.
*
* Throws if the stream ends without it: returning would resolve as though the
* run had completed, and the caller would then fail on a later assertion
* instead of on the real cause.
*
* Typed structurally so a caller need not import the reactor's event union to
* await its terminal event.
*/
export async function waitForReactorDone(
stream: AsyncIterable<{ type: string }>,
): Promise<void> {
for await (const event of stream) {
if (event.type === "reactor.done") return;
}
throw new Error("reactor event stream ended before reactor.done");
}
96 changes: 96 additions & 0 deletions vendor/intx-authz/src/tool-namespace-invariants.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// How a wildcard grant row and a bare `tool:<name>` row each fare against a
// PINNED tool's runtime gate.
//
// A pinned tool package is loaded through the namespacing loader, which
// rewrites every definition name to `<bundleId>:<name>`. The workflow child
// gates each call on `tool:<call.name>`, so the query it issues for a pinned
// tool is `tool:<bundleId>:<name>`. The deploy-time capability walk, by
// contrast, reads inline `agent.toolFactories`, which carry no bundle context,
// so the rows it contributes are bare `tool:<name>`.
//
// That asymmetry is what makes the namespaced form a step-scoping mechanism:
// a bare row cannot address a pinned tool's gate, and the authority a pinned
// tool actually needs is supplied per step by the sidecar's tool-mark floor,
// derived from the factories that step loaded. These cases pin what a wildcard
// row does to that scoping.

import { describe, test, expect } from "bun:test";

import { evaluateGrants } from "./evaluate";
import type { GrantRule } from "./types";

const BUNDLE_ID = "tp_pinned_bundle";
const TOOL_NAME = "search";
const NAMESPACED_GATE = `tool:${BUNDLE_ID}:${TOOL_NAME}`;
const BARE_GATE = `tool:${TOOL_NAME}`;
const INVOKE = "invoke";

function grant(
overrides: Partial<GrantRule> &
Pick<GrantRule, "resource" | "action" | "effect">,
): GrantRule {
return {
id: `grt_${Math.random().toString(36).slice(2, 10)}`,
origin: "creator",
conditions: null,
expiresAt: null,
roleId: null,
principalId: null,
...overrides,
};
}

describe("a wildcard row reaches a pinned tool's namespaced gate", () => {
test("*/* resolves to allow for tool:<bundleId>:<name>/invoke", async () => {
const result = await evaluateGrants(
[grant({ resource: "*", action: "*", effect: "allow" })],
NAMESPACED_GATE,
INVOKE,
);
expect(result.effect).toBe("allow");
});

test("the walk's bare tool:<name> row does NOT reach that gate", async () => {
const result = await evaluateGrants(
[grant({ resource: BARE_GATE, action: INVOKE, effect: "allow" })],
NAMESPACED_GATE,
INVOKE,
);
expect(result.effect).toBe(null);
});

test("the bare row is operative against a bare, unpinned gate", async () => {
const result = await evaluateGrants(
[grant({ resource: BARE_GATE, action: INVOKE, effect: "allow" })],
BARE_GATE,
INVOKE,
);
expect(result.effect).toBe("allow");
});

test("a wildcard row reaches a second bundle's tool the step never loaded", async () => {
const result = await evaluateGrants(
[grant({ resource: "*", action: "*", effect: "allow" })],
"tool:tp_other_bundle:transfer",
INVOKE,
);
expect(result.effect).toBe("allow");
});
});

describe("the tool-mark floor still outranks a wildcard row", () => {
test("an ask floor beats */* allow at higher specificity", async () => {
const floor = grant({
resource: NAMESPACED_GATE,
action: INVOKE,
effect: "ask",
});
const wildcard = grant({ resource: "*", action: "*", effect: "allow" });
const result = await evaluateGrants(
[wildcard, floor],
NAMESPACED_GATE,
INVOKE,
);
expect(result.effect).toBe("ask");
});
});
Loading
Loading