@intentic/sandbox-contract 1.309.0 → 1.311.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -159
- package/dist/contracts/accounts.contract.d.ts +3 -0
- package/dist/contracts/accounts.contract.d.ts.map +1 -1
- package/dist/contracts/agent.contract.d.ts +15 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.d.ts +596 -1
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +13 -2
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/capabilities.contract.d.ts +2 -0
- package/dist/contracts/capabilities.contract.d.ts.map +1 -1
- package/dist/contracts/device.contract.d.ts +1 -0
- package/dist/contracts/device.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +45 -0
- package/dist/contracts/runner.contract.d.ts.map +1 -1
- package/dist/contracts/settings.contract.d.ts +11 -15
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +59 -0
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/events/agent-events.d.ts +20 -0
- package/dist/events/agent-events.d.ts.map +1 -1
- package/dist/events/agent-events.js +6 -1
- package/dist/events/agent-events.js.map +1 -1
- package/dist/events/system-events.d.ts +48 -0
- package/dist/events/system-events.d.ts.map +1 -1
- package/dist/events/transcript.d.ts +5 -0
- package/dist/events/transcript.d.ts.map +1 -1
- package/dist/events/transcript.js +9 -1
- package/dist/events/transcript.js.map +1 -1
- package/dist/front/generated/wire.d.ts +74 -0
- package/dist/front/generated/wire.d.ts.map +1 -0
- package/dist/front/generated/wire.js +2 -0
- package/dist/front/generated/wire.js.map +1 -0
- package/dist/index.d.ts +717 -74
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/policy/command-run.d.ts +1 -1
- package/dist/policy/command-run.d.ts.map +1 -1
- package/dist/policy/command-run.js +7 -4
- package/dist/policy/command-run.js.map +1 -1
- package/dist/protocol/ingress-protocol.d.ts.map +1 -1
- package/dist/protocol/ingress-protocol.js +15 -0
- package/dist/protocol/ingress-protocol.js.map +1 -1
- package/dist/protocol/upgrade-relay.d.ts +4 -0
- package/dist/protocol/upgrade-relay.d.ts.map +1 -0
- package/dist/protocol/upgrade-relay.js +25 -0
- package/dist/protocol/upgrade-relay.js.map +1 -0
- package/dist/schemas/agents.d.ts +72 -3
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +10 -7
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +24 -0
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/capabilities.d.ts +3 -0
- package/dist/schemas/capabilities.d.ts.map +1 -1
- package/dist/schemas/capabilities.js +4 -0
- package/dist/schemas/capabilities.js.map +1 -1
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/devices.d.ts +32 -6
- package/dist/schemas/devices.d.ts.map +1 -1
- package/dist/schemas/devices.js +8 -6
- package/dist/schemas/devices.js.map +1 -1
- package/dist/schemas/keep-warm.d.ts +68 -0
- package/dist/schemas/keep-warm.d.ts.map +1 -0
- package/dist/schemas/keep-warm.js +74 -0
- package/dist/schemas/keep-warm.js.map +1 -0
- package/dist/schemas/metrics.d.ts +24 -25
- package/dist/schemas/metrics.d.ts.map +1 -1
- package/dist/schemas/metrics.js +6 -31
- package/dist/schemas/metrics.js.map +1 -1
- package/dist/schemas/providers/provider-oauth.d.ts +3 -0
- package/dist/schemas/providers/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/providers/provider-oauth.js +4 -0
- package/dist/schemas/providers/provider-oauth.js.map +1 -1
- package/dist/schemas/providers/usage.d.ts +6 -0
- package/dist/schemas/providers/usage.d.ts.map +1 -1
- package/dist/schemas/providers/usage.js +4 -0
- package/dist/schemas/providers/usage.js.map +1 -1
- package/dist/schemas/settings.d.ts +13 -29
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +48 -11
- package/dist/schemas/settings.js.map +1 -1
- package/dist/state/definition.d.ts +24 -20
- package/dist/state/definition.d.ts.map +1 -1
- package/dist/state/fix-stance.d.ts.map +1 -1
- package/dist/state/fix-stance.js +1 -0
- package/dist/state/fix-stance.js.map +1 -1
- package/dist/state/runtime-state.d.ts +3 -0
- package/dist/state/runtime-state.d.ts.map +1 -1
- package/dist/state/runtime-state.js +1 -0
- package/dist/state/runtime-state.js.map +1 -1
- package/dist/state/workspace-state.d.ts +0 -7
- package/dist/state/workspace-state.d.ts.map +1 -1
- package/dist/state/workspace-state.js +0 -8
- package/dist/state/workspace-state.js.map +1 -1
- package/dist/text/transcript-fold.d.ts +2 -0
- package/dist/text/transcript-fold.d.ts.map +1 -1
- package/dist/text/transcript-fold.js +20 -7
- package/dist/text/transcript-fold.js.map +1 -1
- package/dist/time/zone.d.ts +1 -0
- package/dist/time/zone.d.ts.map +1 -1
- package/dist/time/zone.js +5 -0
- package/dist/time/zone.js.map +1 -1
- package/package.json +27 -5
- package/src/chores/chores.test.ts +0 -1
- package/src/chores/digest.test.ts +0 -1
- package/src/chores/probes.test.ts +0 -1
- package/src/chores/stack.test.ts +0 -1
- package/src/chores/verdict.test.ts +0 -1
- package/src/contracts/agents.contract.ts +14 -2
- package/src/events/agent-events.ts +9 -1
- package/src/events/agent-words.test.ts +0 -1
- package/src/events/resume.test.ts +0 -1
- package/src/events/transcript.ts +14 -3
- package/src/events/watch-wake.test.ts +0 -1
- package/src/front/generated/wire.ts +73 -0
- package/src/ids/conversation-ids.test.ts +1 -2
- package/src/ids/hostnames.fixture.json +20 -0
- package/src/ids/hostnames.test.ts +20 -1
- package/src/ids/share-paths.test.ts +0 -1
- package/src/ids/tunnel-ids.test.ts +0 -1
- package/src/index.ts +1 -3
- package/src/models/agent-catalog.test.ts +0 -1
- package/src/models/capability-ledger.test.ts +0 -1
- package/src/models/model-label.test.ts +0 -1
- package/src/models/model-offer.test.ts +0 -1
- package/src/models/model-order.test.ts +0 -1
- package/src/models/model-pins.test.ts +0 -1
- package/src/models/model-roles.test.ts +0 -1
- package/src/models/plan-pools.test.ts +0 -1
- package/src/models/provider-specs.test.ts +0 -1
- package/src/policy/batch-runs.test.ts +0 -1
- package/src/policy/command-classes.test.ts +0 -1
- package/src/policy/command-run.test.ts +35 -0
- package/src/policy/command-run.ts +10 -5
- package/src/policy/credential-material.test.ts +0 -1
- package/src/policy/fence-paths.test.ts +0 -1
- package/src/policy/overlay-lint.test.ts +0 -1
- package/src/policy/owner-ticket.test.ts +0 -1
- package/src/policy/persona-home.test.ts +0 -1
- package/src/policy/reserved-servers.test.ts +0 -1
- package/src/policy/safety-policy.test.ts +0 -1
- package/src/policy/search-globs.test.ts +0 -1
- package/src/protocol/container-requirements.test.ts +0 -1
- package/src/protocol/ingress-contract.test.ts +0 -1
- package/src/protocol/ingress-protocol.test.ts +45 -2
- package/src/protocol/ingress-protocol.ts +25 -4
- package/src/protocol/peer-dial.test.ts +1 -2
- package/src/protocol/peer-mcp-server.test.ts +0 -1
- package/src/protocol/routes.test.ts +6 -3
- package/src/protocol/tunnel-heartbeat.test.ts +2 -3
- package/src/protocol/upgrade-relay.ts +28 -0
- package/src/schemas/agent.test.ts +0 -1
- package/src/schemas/agents.ts +14 -7
- package/src/schemas/areas.test.ts +1 -2
- package/src/schemas/capabilities.ts +6 -0
- package/src/schemas/ci.ts +3 -9
- package/src/schemas/context-trim.test.ts +0 -1
- package/src/schemas/dev-rebuild-log.test.ts +0 -1
- package/src/schemas/devices.ts +16 -9
- package/src/schemas/git/git.test.ts +0 -1
- package/src/schemas/keep-warm.test.ts +68 -0
- package/src/schemas/keep-warm.ts +124 -0
- package/src/schemas/metrics.ts +33 -64
- package/src/schemas/providers/provider-oauth.ts +6 -0
- package/src/schemas/providers/usage.ts +7 -0
- package/src/schemas/settings.test.ts +41 -7
- package/src/schemas/settings.ts +78 -44
- package/src/schemas/version-seam.test.ts +0 -1
- package/src/state/contract-lock.test.ts +0 -1
- package/src/state/fix-attempt-plan.test.ts +0 -1
- package/src/state/fix-stance.test.ts +14 -1
- package/src/state/fix-stance.ts +2 -0
- package/src/state/landed-commit.test.ts +0 -1
- package/src/state/runtime-state.test.ts +0 -1
- package/src/state/runtime-state.ts +4 -0
- package/src/state/versions.test.ts +0 -1
- package/src/state/workspace-state.test.ts +0 -3
- package/src/state/workspace-state.ts +0 -14
- package/src/text/archives.test.ts +0 -1
- package/src/text/documents.test.ts +0 -1
- package/src/text/embed.test.ts +2 -3
- package/src/text/emoji.test.ts +0 -1
- package/src/text/mentions.test.ts +0 -1
- package/src/text/path-refs.test.ts +0 -1
- package/src/text/title.test.ts +0 -1
- package/src/text/transcript-fold.test.ts +32 -4
- package/src/text/transcript-fold.ts +39 -9
- package/src/text/whisper.test.ts +0 -1
- package/src/text/workflow-faults.test.ts +0 -1
- package/src/time/zone.test.ts +0 -1
- package/src/time/zone.ts +7 -0
- package/src/webext/index.ts +1 -1
- package/dist/contracts/prepush.contract.d.ts +0 -32
- package/dist/contracts/prepush.contract.d.ts.map +0 -1
- package/dist/contracts/prepush.contract.js +0 -41
- package/dist/contracts/prepush.contract.js.map +0 -1
- package/src/contracts/prepush.contract.ts +0 -58
package/src/index.ts
CHANGED
|
@@ -28,7 +28,6 @@ import { loopsContract } from "./contracts/loops.contract.js";
|
|
|
28
28
|
import { panelsContract } from "./contracts/panels.contract.js";
|
|
29
29
|
import { portsContract } from "./contracts/ports.contract.js";
|
|
30
30
|
import { publicContract } from "./contracts/public.contract.js";
|
|
31
|
-
import { prepushContract } from "./contracts/prepush.contract.js";
|
|
32
31
|
import { netdiskContract } from "./contracts/netdisk.contract.js";
|
|
33
32
|
import { providersContract } from "./contracts/providers.contract.js";
|
|
34
33
|
import { pushContract } from "./contracts/push.contract.js";
|
|
@@ -89,7 +88,6 @@ export { loopsContract } from "./contracts/loops.contract.js";
|
|
|
89
88
|
export { panelsContract } from "./contracts/panels.contract.js";
|
|
90
89
|
export { portsContract } from "./contracts/ports.contract.js";
|
|
91
90
|
export { publicContract } from "./contracts/public.contract.js";
|
|
92
|
-
export { prepushContract } from "./contracts/prepush.contract.js";
|
|
93
91
|
export { providersContract, type RunnableProviders, RunnableProvidersSchema } from "./contracts/providers.contract.js";
|
|
94
92
|
export { pushContract } from "./contracts/push.contract.js";
|
|
95
93
|
export { safetyContract } from "./contracts/safety.contract.js";
|
|
@@ -218,6 +216,7 @@ export * from "./schemas/system-prompt.js";
|
|
|
218
216
|
export * from "./schemas/system.js";
|
|
219
217
|
export * from "./schemas/terminal.js";
|
|
220
218
|
export * from "./schemas/turn-break.js";
|
|
219
|
+
export * from "./schemas/keep-warm.js";
|
|
221
220
|
export * from "./schemas/providers/usage.js";
|
|
222
221
|
export * from "./schemas/vpn.js";
|
|
223
222
|
export * from "./schemas/webext.js";
|
|
@@ -272,7 +271,6 @@ export const sandboxContract = {
|
|
|
272
271
|
panels: panelsContract,
|
|
273
272
|
ports: portsContract,
|
|
274
273
|
public: publicContract,
|
|
275
|
-
prepush: prepushContract,
|
|
276
274
|
providers: providersContract,
|
|
277
275
|
push: pushContract,
|
|
278
276
|
secrets: secretsContract,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { test, expect } from "bun:test";
|
|
2
1
|
import { compareModelIds, compareUnrankedModelIds, familyOf, namesThinking, releaseOf, tierRankOf } from "./model-order.js";
|
|
3
2
|
|
|
4
3
|
// The order every provider's catalog is served and browsed in; only Anthropic's catalog arrives ranked, the rest hand
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { PROVIDER_BRAND_PATHS } from "@intentic/constants";
|
|
2
|
-
import { describe, test, expect } from "bun:test";
|
|
3
2
|
import { accessFor, capabilitiesOf, harnessChoosable, PROVIDER_ACCESS, PROVIDER_VENDOR, PROVIDERS, providerLabel } from "./agent-catalog.js";
|
|
4
3
|
import {
|
|
5
4
|
ACCESS_COST,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
1
|
import { classifyCommand, type CommandContext, type CommandSpan, matchCommand } from "./command-classes.js";
|
|
3
2
|
|
|
4
3
|
// Every call states a locus (now required); these default to `device`, the wider reading, so an assertion made here
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CommandRun } from "../schemas/ci.js";
|
|
2
|
+
import { followCommandRun } from "./command-run.js";
|
|
3
|
+
|
|
4
|
+
const RUNNING: CommandRun = { status: "running", command: "pnpm test", output: "" };
|
|
5
|
+
const PASSED: CommandRun = { status: "passed", command: "pnpm test", output: "ok" };
|
|
6
|
+
|
|
7
|
+
test("a poll that fails is reported and retried, and the run is still followed to its verdict", async () => {
|
|
8
|
+
const answers: Array<CommandRun | Error> = [new Error("daemon restarting"), RUNNING, PASSED];
|
|
9
|
+
const errors: unknown[] = [];
|
|
10
|
+
const settled = await followCommandRun(
|
|
11
|
+
async () => {
|
|
12
|
+
const next = answers.shift();
|
|
13
|
+
if (next instanceof Error) {
|
|
14
|
+
throw next;
|
|
15
|
+
}
|
|
16
|
+
return next as CommandRun;
|
|
17
|
+
},
|
|
18
|
+
{ intervalMs: 1, onError: (cause) => errors.push(cause) },
|
|
19
|
+
);
|
|
20
|
+
expect(settled).toEqual(PASSED);
|
|
21
|
+
expect(errors).toEqual([new Error("daemon restarting")]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("a throw from onState is the caller's bug and reaches the caller, not a poll failure retried forever", async () => {
|
|
25
|
+
const errors: unknown[] = [];
|
|
26
|
+
const following = followCommandRun(async () => RUNNING, {
|
|
27
|
+
intervalMs: 1,
|
|
28
|
+
onState: () => {
|
|
29
|
+
throw new TypeError("state.session is undefined");
|
|
30
|
+
},
|
|
31
|
+
onError: (cause) => errors.push(cause),
|
|
32
|
+
});
|
|
33
|
+
await expect(following).rejects.toThrow("state.session is undefined");
|
|
34
|
+
expect(errors).toEqual([]);
|
|
35
|
+
});
|
|
@@ -10,8 +10,9 @@ export interface FollowRunOptions<R extends CommandRun> {
|
|
|
10
10
|
readonly signal?: AbortSignal | undefined;
|
|
11
11
|
// Every state read, settled or not, for a surface that draws progress.
|
|
12
12
|
readonly onState?: ((run: R) => void) | undefined;
|
|
13
|
-
// A poll that failed, not a run that failed: the run is still going, and following continues.
|
|
14
|
-
|
|
13
|
+
// A poll that failed, not a run that failed: the run is still going, and following continues. Required, since a
|
|
14
|
+
// loop that retries a failing read forever with nobody told looks exactly like a run that is still going.
|
|
15
|
+
readonly onError: (cause: unknown) => void;
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
// Follows a run to its verdict: reads state until it is no longer running. The one shared loop, so a dropped poll isn't
|
|
@@ -22,14 +23,18 @@ export const followCommandRun = async <R extends CommandRun>(read: () => Promise
|
|
|
22
23
|
if (signal?.aborted === true) {
|
|
23
24
|
return undefined;
|
|
24
25
|
}
|
|
26
|
+
// Only the read is a poll that may fail and be retried; a throw from `onState` is the caller's own bug.
|
|
27
|
+
let run: R | undefined;
|
|
25
28
|
try {
|
|
26
|
-
|
|
29
|
+
run = await read();
|
|
30
|
+
} catch (cause) {
|
|
31
|
+
onError(cause);
|
|
32
|
+
}
|
|
33
|
+
if (run !== undefined) {
|
|
27
34
|
onState?.(run);
|
|
28
35
|
if (run.status !== "running") {
|
|
29
36
|
return run;
|
|
30
37
|
}
|
|
31
|
-
} catch (cause) {
|
|
32
|
-
onError?.(cause);
|
|
33
38
|
}
|
|
34
39
|
await sleep(intervalMs, { signal });
|
|
35
40
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { test, expect } from "bun:test";
|
|
2
1
|
import { hasOfficialBase, invalidExtensionFragment, isOfficialSandboxImage, lintOverlay, overlayBase, rewriteOverlayBase } from "./overlay-lint.js";
|
|
3
2
|
|
|
4
3
|
test("accepts a RUN/ENV-only fragment, comments and blank lines included", () => {
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
-
import { describe, it, expect } from "bun:test";
|
|
3
2
|
import { mintReachabilityGrant, verifyReachabilityGrant } from "../protocol/ingress-contract.js";
|
|
4
3
|
import { isOwnerTicket, mintOwnerTicket, OWNER_TICKET_TTL_MS, publicKeyPemOf, verifyOwnerTicket } from "./owner-ticket.js";
|
|
5
4
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
1
|
import { COMMAND_CLASS_LABELS, COMMAND_CLASS_PATTERNS } from "./command-classes.js";
|
|
3
2
|
import { COMMAND_RULE_CATALOG, DEFAULT_SAFETY_POLICY, hardRuleClasses } from "./safety-policy.js";
|
|
4
3
|
import { CommandClassSchema, CommandLocusSchema } from "../schemas/agent.js";
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { generateKeyPairSync } from "node:crypto";
|
|
2
|
-
import { describe, it, expect } from "bun:test";
|
|
3
2
|
import { hostOwnerId, mintReachabilityGrant, verifyReachabilityGrant } from "./ingress-contract.js";
|
|
4
3
|
|
|
5
4
|
const pemPair = (): { privateKey: string; publicKey: string } => {
|
|
@@ -2,7 +2,6 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
2
2
|
import { createServer, type IncomingMessage, request as h1Request, type Server, type ServerResponse } from "node:http";
|
|
3
3
|
import type { AddressInfo } from "node:net";
|
|
4
4
|
import { type Duplex, duplexPair } from "node:stream";
|
|
5
|
-
import { test, expect, beforeAll, afterAll } from "bun:test";
|
|
6
5
|
import { waitFor } from "@intentic/testing/bun";
|
|
7
6
|
import { INITIAL_WINDOW_SIZE, openIngressSession, serveIngressSession } from "./ingress-protocol.js";
|
|
8
7
|
|
|
@@ -149,9 +148,25 @@ const target = createServer((request: IncomingMessage, response: ServerResponse)
|
|
|
149
148
|
|
|
150
149
|
const cancelled = gate<string>();
|
|
151
150
|
|
|
151
|
+
// `/late` holds its handshake until released, so the session can die while the loopback upgrade is still pending.
|
|
152
|
+
const lateArrived = gate();
|
|
153
|
+
const lateReleased = gate();
|
|
154
|
+
let lateTornDown = false;
|
|
155
|
+
|
|
152
156
|
// Hand-written HTTP/1.1 upgrade carrying the 101 head plus the raw bytes after it. `/refuse` answers instead of
|
|
153
157
|
// upgrading.
|
|
154
158
|
target.on("upgrade", (request: IncomingMessage, socket: Duplex, head: Buffer) => {
|
|
159
|
+
if ((request.url ?? "") === "/late") {
|
|
160
|
+
socket.on("error", () => {});
|
|
161
|
+
socket.on("close", () => (lateTornDown = true));
|
|
162
|
+
lateArrived.open();
|
|
163
|
+
void lateReleased.opened.then(() => {
|
|
164
|
+
if (!socket.destroyed) {
|
|
165
|
+
socket.write("HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n");
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
155
170
|
if ((request.url ?? "") === "/refuse") {
|
|
156
171
|
socket.end("HTTP/1.1 404 Not Found\r\nContent-Type: text/plain\r\nContent-Length: 7\r\n\r\nno dice");
|
|
157
172
|
return;
|
|
@@ -180,7 +195,7 @@ target.on("upgrade", (request: IncomingMessage, socket: Duplex, head: Buffer) =>
|
|
|
180
195
|
// One session; every request is routed through it.
|
|
181
196
|
const front = async (
|
|
182
197
|
targetPort: number,
|
|
183
|
-
): Promise<{ readonly server: Server; readonly poison: (bytes: Buffer) => void; readonly close: () => void }> => {
|
|
198
|
+
): Promise<{ readonly server: Server; readonly poison: (bytes: Buffer) => void; readonly sever: () => void; readonly close: () => void }> => {
|
|
184
199
|
const [edgeSide, daemonSide] = duplexPair();
|
|
185
200
|
const daemon = await serveIngressSession(daemonSide, { targetPort });
|
|
186
201
|
const session = await openIngressSession(edgeSide);
|
|
@@ -203,6 +218,8 @@ const front = async (
|
|
|
203
218
|
server,
|
|
204
219
|
// Writes garbage onto the wire the edge reads, simulating a wedged or corrupted peer.
|
|
205
220
|
poison: (bytes: Buffer) => void daemonSide.write(bytes),
|
|
221
|
+
// Cuts the tunnel under the daemon, which takes its h2 session and every stream on it down.
|
|
222
|
+
sever: () => void daemonSide.destroy(),
|
|
206
223
|
close: () => {
|
|
207
224
|
session.close();
|
|
208
225
|
daemon.close();
|
|
@@ -440,6 +457,32 @@ test("a browser that gives up cancels the stream all the way to the target", asy
|
|
|
440
457
|
await expect(cancelled.opened).resolves.toBe("aborted");
|
|
441
458
|
});
|
|
442
459
|
|
|
460
|
+
// The handshake lands after its stream is gone: answering it throws ERR_HTTP2_INVALID_STREAM from an event callback.
|
|
461
|
+
test("a session that dies mid-handshake tears the loopback upgrade down instead of answering into a dead stream", async () => {
|
|
462
|
+
const own = await front((target.address() as AddressInfo).port);
|
|
463
|
+
const ownPort = await listen(own.server);
|
|
464
|
+
const request = h1Request({
|
|
465
|
+
host: "127.0.0.1",
|
|
466
|
+
port: ownPort,
|
|
467
|
+
path: "/late",
|
|
468
|
+
headers: { host: HOST, connection: "Upgrade", upgrade: "websocket", "sec-websocket-key": "x" },
|
|
469
|
+
agent: false,
|
|
470
|
+
});
|
|
471
|
+
request.on("error", () => {});
|
|
472
|
+
request.end();
|
|
473
|
+
await lateArrived.opened;
|
|
474
|
+
|
|
475
|
+
own.sever();
|
|
476
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
477
|
+
lateReleased.open();
|
|
478
|
+
|
|
479
|
+
await waitFor(() => expect(lateTornDown).toBe(true));
|
|
480
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
481
|
+
expect(uncaught).toStrictEqual([]);
|
|
482
|
+
request.destroy();
|
|
483
|
+
own.close();
|
|
484
|
+
});
|
|
485
|
+
|
|
443
486
|
test("a tunnel whose target is not listening fails the exchange rather than answering for it", async () => {
|
|
444
487
|
const dead = createServer();
|
|
445
488
|
const deadPort = await listen(dead);
|
|
@@ -10,9 +10,9 @@ import {
|
|
|
10
10
|
import { type AddressInfo, createServer as createNetServer, connect as netConnect, type Socket } from "node:net";
|
|
11
11
|
import { Duplex } from "node:stream";
|
|
12
12
|
|
|
13
|
-
// Both
|
|
14
|
-
//
|
|
15
|
-
// interleave several sandboxes' hosts.
|
|
13
|
+
// Both halves of the ingress tunnel's h2 session over any node Duplex, in one file so the wire has one owner: the edge's,
|
|
14
|
+
// and a daemon half its suites dial (a sandbox runs the Rust one, _sandbox/front tunnel.rs, held to this by its test).
|
|
15
|
+
// Routing is per h2 stream, since one edge connection can interleave several sandboxes' hosts.
|
|
16
16
|
|
|
17
17
|
// Header hygiene
|
|
18
18
|
|
|
@@ -308,7 +308,7 @@ export const openIngressSession = async (duplex: Duplex): Promise<IngressSession
|
|
|
308
308
|
};
|
|
309
309
|
};
|
|
310
310
|
|
|
311
|
-
// The daemon half: an h2 server over the tunnel, onto
|
|
311
|
+
// The daemon half the edge's suites dial: an h2 server over the tunnel, onto a local listener
|
|
312
312
|
|
|
313
313
|
export interface ServeIngressSessionOptions {
|
|
314
314
|
// The daemon's own listener; every stream lands there as a plain HTTP/1.1 request or upgrade.
|
|
@@ -340,6 +340,10 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
|
|
|
340
340
|
server.on("error", () => duplex.destroy());
|
|
341
341
|
server.on("session", (session) => session.setLocalWindowSize(CONNECTION_WINDOW_SIZE));
|
|
342
342
|
|
|
343
|
+
// The loopback's answer can outlive its stream when the session dies, and respond/write on a dead stream throws
|
|
344
|
+
// ERR_HTTP2_INVALID_STREAM from an event callback, where nothing catches it.
|
|
345
|
+
const gone = (stream: ServerHttp2Stream): boolean => stream.destroyed || stream.closed;
|
|
346
|
+
|
|
343
347
|
const forwardToLoopback = (stream: ServerHttp2Stream, headers: IncomingHttpHeaders): void => {
|
|
344
348
|
const local = h1Request({
|
|
345
349
|
host: LOOPBACK,
|
|
@@ -353,6 +357,10 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
|
|
|
353
357
|
});
|
|
354
358
|
stream.pipe(local);
|
|
355
359
|
local.on("response", (response) => {
|
|
360
|
+
if (gone(stream)) {
|
|
361
|
+
response.destroy();
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
356
364
|
stream.respond({ [constants.HTTP2_HEADER_STATUS]: response.statusCode ?? 502, ...endToEnd(response.headers) });
|
|
357
365
|
response.pipe(stream);
|
|
358
366
|
});
|
|
@@ -364,6 +372,9 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
|
|
|
364
372
|
// means this — on a reset, node fires `aborted` before `finish` and `close`, so a
|
|
365
373
|
// `writableEnded`/`writableFinished` guard there would miss it.
|
|
366
374
|
stream.on("aborted", () => local.destroy());
|
|
375
|
+
// `aborted` fires only while the stream's writable side is open; `close` covers every other way it dies, and a
|
|
376
|
+
// request whose answer already completed ignores the destroy.
|
|
377
|
+
stream.on("close", () => local.destroy());
|
|
367
378
|
};
|
|
368
379
|
|
|
369
380
|
const spliceUpgrade = (stream: ServerHttp2Stream, headers: IncomingHttpHeaders): void => {
|
|
@@ -372,6 +383,10 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
|
|
|
372
383
|
// pool the request path above uses.
|
|
373
384
|
const local = h1Request({ host: LOOPBACK, port: options.targetPort, method: head.method, path: head.path, headers: head.headers, agent: false });
|
|
374
385
|
local.on("upgrade", (response, socket, first) => {
|
|
386
|
+
if (gone(stream)) {
|
|
387
|
+
socket.destroy();
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
375
390
|
stream.respond({ [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_OK });
|
|
376
391
|
// Response head written verbatim (see serializeHead), then any bytes already sent past it, then the pipe.
|
|
377
392
|
stream.write(serializeHead(response, NOTHING_DROPPED, []));
|
|
@@ -383,11 +398,17 @@ export const serveIngressSession = async (duplex: Duplex, options: ServeIngressS
|
|
|
383
398
|
// The local server declined to upgrade; its answer goes to the browser with framing that matches the already
|
|
384
399
|
// de-chunked body.
|
|
385
400
|
local.on("response", (response) => {
|
|
401
|
+
if (gone(stream)) {
|
|
402
|
+
response.destroy();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
386
405
|
stream.respond({ [constants.HTTP2_HEADER_STATUS]: constants.HTTP_STATUS_OK });
|
|
387
406
|
stream.write(serializeHead(response, DECLINED_UPGRADE_DROP, DECLINED_UPGRADE_ADD));
|
|
388
407
|
response.pipe(stream);
|
|
389
408
|
});
|
|
390
409
|
local.on("error", () => stream.close(constants.NGHTTP2_CONNECT_ERROR));
|
|
410
|
+
// Before the handshake answers, nothing else ties the loopback request to the stream; once spliced, a no-op.
|
|
411
|
+
stream.on("close", () => local.destroy());
|
|
391
412
|
// An upgrade request carries no body; the client is waiting on the handshake before it says anything else.
|
|
392
413
|
local.end();
|
|
393
414
|
};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { test, expect, mock, jest } from "bun:test";
|
|
2
1
|
import { waitFor, advanceTimersByTimeAsync } from "@intentic/testing/bun";
|
|
3
2
|
import { dialPeer, LONG_OUTAGE_ATTEMPTS, LONG_OUTAGE_MS, PEER_TRY_AGAIN, type SocketLike } from "./peer-dial.js";
|
|
4
3
|
|
|
@@ -64,7 +63,7 @@ const dialling = (delay?: number) => {
|
|
|
64
63
|
const sockets: FakeSocket[] = [];
|
|
65
64
|
const attached: FakeSocket[] = [];
|
|
66
65
|
const said: string[] = [];
|
|
67
|
-
const revoked =
|
|
66
|
+
const revoked = jest.fn();
|
|
68
67
|
const link = dialPeer<FakeSocket>({
|
|
69
68
|
open: async () => {
|
|
70
69
|
const socket = new FakeSocket();
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { eventIterator, oc } from "@orpc/contract";
|
|
2
|
-
import { describe, it, expect } from "bun:test";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
import { SANDBOX_ROUTE_NAMES, SANDBOX_ROUTE_SHAPES, SANDBOX_ROUTES, sandboxRouteName } from "../index.js";
|
|
5
4
|
import { procedure } from "./route-meta.js";
|
|
@@ -34,8 +33,12 @@ describe(`contractRoutes`, () => {
|
|
|
34
33
|
|
|
35
34
|
it(`carries the policy a procedure declared, merged over its builder's`, () => {
|
|
36
35
|
const operated = procedure.meta({ agent: true, control: `never` });
|
|
37
|
-
const routes = contractRoutes({
|
|
38
|
-
|
|
36
|
+
const routes = contractRoutes({
|
|
37
|
+
vpn: { connect: operated.route({ method: "POST", path: "/vpn/{id}/connect" }).meta({ floor: `collaborator` }) },
|
|
38
|
+
});
|
|
39
|
+
expect(routes).toEqual([
|
|
40
|
+
{ name: `vpn.connect`, method: `POST`, path: `/vpn/{id}/connect`, meta: { agent: true, control: `never`, floor: `collaborator` } },
|
|
41
|
+
]);
|
|
39
42
|
});
|
|
40
43
|
|
|
41
44
|
it(`ignores non-procedure members rather than inventing routes for them`, () => {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import { describe, test, expect, mock } from "bun:test";
|
|
2
1
|
import { createHeartbeat, DEAD_AFTER_MS } from "./tunnel-heartbeat.js";
|
|
3
2
|
|
|
4
3
|
// The clock is injected, so these read as "what happens after N seconds of silence" rather than as timer
|
|
5
4
|
// plumbing — which is the only question the heartbeat answers.
|
|
6
5
|
const world = (deadAfterMs = DEAD_AFTER_MS) => {
|
|
7
6
|
let clock = 0;
|
|
8
|
-
const ping =
|
|
9
|
-
const onDead =
|
|
7
|
+
const ping = jest.fn();
|
|
8
|
+
const onDead = jest.fn();
|
|
10
9
|
const heartbeat = createHeartbeat({ ping, onDead, deadAfterMs, now: () => clock });
|
|
11
10
|
return { heartbeat, ping, onDead, advance: (ms: number) => (clock += ms) };
|
|
12
11
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ClientRequest } from "node:http";
|
|
2
|
+
import type { Duplex } from "node:stream";
|
|
3
|
+
|
|
4
|
+
// `upstream` is the client's handshake replayed; a 101 is echoed and bytes flow raw, any other answer ends the socket.
|
|
5
|
+
export const relayUpgrade = (upstream: ClientRequest, socket: Duplex, head: Buffer): void => {
|
|
6
|
+
upstream.on("error", () => socket.destroy());
|
|
7
|
+
upstream.on("response", (answer) => {
|
|
8
|
+
socket.end(`HTTP/1.1 ${answer.statusCode ?? 502} ${answer.statusMessage ?? ""}\r\n\r\n`);
|
|
9
|
+
});
|
|
10
|
+
upstream.on("upgrade", (answer, upstreamSocket, upstreamHead) => {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
for (let index = 0; index < answer.rawHeaders.length; index += 2) {
|
|
13
|
+
lines.push(`${answer.rawHeaders[index]}: ${answer.rawHeaders[index + 1]}`);
|
|
14
|
+
}
|
|
15
|
+
socket.write(`HTTP/1.1 101 Switching Protocols\r\n${lines.join("\r\n")}\r\n\r\n`);
|
|
16
|
+
if (upstreamHead.length > 0) {
|
|
17
|
+
socket.write(upstreamHead);
|
|
18
|
+
}
|
|
19
|
+
if (head.length > 0) {
|
|
20
|
+
upstreamSocket.write(head);
|
|
21
|
+
}
|
|
22
|
+
upstreamSocket.on("error", () => socket.destroy());
|
|
23
|
+
socket.on("error", () => upstreamSocket.destroy());
|
|
24
|
+
upstreamSocket.pipe(socket);
|
|
25
|
+
socket.pipe(upstreamSocket);
|
|
26
|
+
});
|
|
27
|
+
upstream.end();
|
|
28
|
+
};
|
package/src/schemas/agents.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { AgentHarnessSchema, AgentOriginSchema, AgentProviderSchema, ConversationQueueSchema, ForkedFromSchema } from "./agent.js";
|
|
4
4
|
import { LoopStateSchema } from "./loops.js";
|
|
5
5
|
import { LimitPolicySchema, RetryPolicySchema, TurnBreakPolicySchema, TurnBreakSchema } from "./turn-break.js";
|
|
6
|
+
import { KeepWarmSchema } from "./keep-warm.js";
|
|
6
7
|
import { EMOJI_MAX_LENGTH, isSingleEmoji } from "../text/emoji.js";
|
|
7
8
|
// A fleet agent is any conversation with a registry entry, keyed by conversationId. Isolated ones own a git worktree
|
|
8
9
|
// (branch agent/<id>); workspace conversations have none, but both share one status/activity/cost lifecycle.
|
|
@@ -329,11 +330,24 @@ export const AgentSummarySchema = z.object({
|
|
|
329
330
|
.object({
|
|
330
331
|
at: z.number().describe("When its last request touched the provider's prompt cache, in milliseconds."),
|
|
331
332
|
ttlMs: z.number().describe("How long that entry lives from `at`, in milliseconds."),
|
|
333
|
+
rollsAt: z
|
|
334
|
+
.number()
|
|
335
|
+
.optional()
|
|
336
|
+
.describe(
|
|
337
|
+
"When the date written into the agent's prompt next changes, in milliseconds (midnight where the agent runs). Past it the next turn sends a different prompt, so nothing kept before it is read again.",
|
|
338
|
+
),
|
|
339
|
+
keepable: z
|
|
340
|
+
.boolean()
|
|
341
|
+
.optional()
|
|
342
|
+
.describe("Whether this sandbox can keep this cache warm: it holds the last turn's exact request to replay, on a Claude subscription account."),
|
|
332
343
|
})
|
|
333
344
|
.optional()
|
|
334
345
|
.describe(
|
|
335
346
|
"When this conversation's prompt cache was last kept alive and how long it lasts, which together say when picking the conversation up stops being cheap. Absent when the provider publishes nothing to ground it on.",
|
|
336
347
|
),
|
|
348
|
+
keepWarm: KeepWarmSchema.optional().describe(
|
|
349
|
+
"Whether the sandbox is keeping this conversation's prompt cache warm while it sits idle, how that is going, or why it stopped. Absent when nobody asked for it.",
|
|
350
|
+
),
|
|
337
351
|
activity: AgentActivitySchema.optional().describe("What it is doing at this moment."),
|
|
338
352
|
// Beside `activity` because both answer "where is it", one in a sentence and one as a fraction. Live while a turn
|
|
339
353
|
// runs, and left standing after it, so a settled card still says how far the work got.
|
|
@@ -517,13 +531,6 @@ export const AgentToolChildrenQuerySchema = AgentIdSchema.extend({
|
|
|
517
531
|
toolId: z.string().min(1).describe("Which tool call, by the id its card carries."),
|
|
518
532
|
});
|
|
519
533
|
// Absent `ids` archives every archivable finished agent (the lane's "Clear"); unarchive always names its own ids.
|
|
520
|
-
export const AgentArchiveSchema = z.object({
|
|
521
|
-
ids: z
|
|
522
|
-
.array(z.string().min(1))
|
|
523
|
-
.max(500)
|
|
524
|
-
.optional()
|
|
525
|
-
.describe("Which conversations to put away. Leave it out for every finished one that can be archived right now."),
|
|
526
|
-
});
|
|
527
534
|
export const AgentIdsSchema = z.object({ ids: z.array(z.string().min(1)).min(1).max(500).describe("Which conversations.") });
|
|
528
535
|
// What actually moved, not the roster afterward: two archives finishing at once would otherwise let a slower snapshot
|
|
529
536
|
// resurrect what a faster one just filed away. Whole summaries, since the receiving surfaces (archive list, detail
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { PUBLIC_DIR, STATE_DIR } from "@intentic/constants";
|
|
2
|
-
import { describe, test, expect } from "bun:test";
|
|
3
2
|
import { AreaFolderSchema } from "./areas.js";
|
|
4
3
|
|
|
5
4
|
// What an area may name. These refusals are load-bearing rather than cosmetic: an area's folders become the fence
|
|
@@ -22,7 +21,7 @@ describe("AreaFolderSchema", () => {
|
|
|
22
21
|
});
|
|
23
22
|
|
|
24
23
|
test("the sandbox's own configuration and its outbox can never be named", () => {
|
|
25
|
-
for (const folder of [STATE_DIR, `${STATE_DIR}/config`, `${STATE_DIR}/config/
|
|
24
|
+
for (const folder of [STATE_DIR, `${STATE_DIR}/config`, `${STATE_DIR}/config/skills`, PUBLIC_DIR, `${PUBLIC_DIR}/site`]) {
|
|
26
25
|
expect(AreaFolderSchema.safeParse(folder).success, folder).toBe(false);
|
|
27
26
|
}
|
|
28
27
|
});
|
|
@@ -354,6 +354,12 @@ export const CapabilityStatusSchema = z.object({
|
|
|
354
354
|
state: CapabilityStateSchema.describe("Whether it is live, still coming up, broken, or switched off."),
|
|
355
355
|
detail: z.string().optional().describe("What is wrong, in words a person can act on."),
|
|
356
356
|
code: z.string().optional().describe("A short marker for that reason, for anything deciding what to do about it."),
|
|
357
|
+
settling: z
|
|
358
|
+
.boolean()
|
|
359
|
+
.optional()
|
|
360
|
+
.describe(
|
|
361
|
+
"True while something under way will move this on its own: a start, a download, a pairing code waiting to be typed. Absent when only a person can move it, or a pushed change will say when it moved.",
|
|
362
|
+
),
|
|
357
363
|
});
|
|
358
364
|
export type CapabilityStatus = z.infer<typeof CapabilityStatusSchema>;
|
|
359
365
|
// The list row: manifest entry plus live status, secrets never returned. `secrets` names which config keys hold a
|
package/src/schemas/ci.ts
CHANGED
|
@@ -151,15 +151,9 @@ export const CiFixResponseSchema = z.object({
|
|
|
151
151
|
conversationId: z.string().describe("The conversation that was opened, already holding the failure. Open it to watch, or attach to its turn."),
|
|
152
152
|
});
|
|
153
153
|
export type CiFixResponse = z.infer<typeof CiFixResponseSchema>;
|
|
154
|
-
//
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
// No stored verdict, nothing polled at rest: a run exists while it runs and is gone after, since the next push asks
|
|
158
|
-
// again.
|
|
159
|
-
|
|
160
|
-
// One shape for any command the daemon runs on a click with a verdict and a quotable tail (the pre-push check, and the
|
|
161
|
-
// push itself via PushRunSchema in schemas/git/git.ts), so the fields the browser reads (terminal, tail, kill vs timeout)
|
|
162
|
-
// can't drift between them.
|
|
154
|
+
// One shape for any command the daemon runs on a click with a verdict and a quotable tail (the push via PushRunSchema in
|
|
155
|
+
// schemas/git/git.ts, which runs the repository's own pre-push hook), so the fields the browser reads (terminal, tail,
|
|
156
|
+
// kill vs timeout) have one definition.
|
|
163
157
|
// idle: nothing has run yet, or the last run was cleared
|
|
164
158
|
// running: live; its output is the terminal's, not this object's
|
|
165
159
|
// passed: exited 0
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { test, expect } from "bun:test";
|
|
2
1
|
import { DEV_REBUILD_EXIT_MARK, DEV_REBUILD_QUIET_MARK, devRebuildLogPath, readDevRebuildLog } from "./devices.js";
|
|
3
2
|
|
|
4
3
|
// The one account a detached rebuild gives of itself. The daemon builds the shell that writes this text and the browser
|