@nebutra/agent-runtime 0.2.1 → 0.2.2
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/LICENSE +21 -676
- package/dist/adapters/index.d.ts +24 -5
- package/dist/adapters/index.js +27 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/chunk-Q62VKHIT.js +178 -0
- package/dist/chunk-Q62VKHIT.js.map +1 -0
- package/dist/{chunk-MUF7ZZTO.js → chunk-R5HOSQUW.js} +2 -1
- package/dist/{chunk-MUF7ZZTO.js.map → chunk-R5HOSQUW.js.map} +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +14 -17
- package/dist/index.js.map +1 -1
- package/dist/orchestration.d.ts +42 -1
- package/dist/orchestration.js +7 -3
- package/dist/sandbox.js +1 -1
- package/package.json +80 -30
- package/.turbo/turbo-build.log +0 -130
- package/.turbo/turbo-test.log +0 -47
- package/.turbo/turbo-typecheck.log +0 -4
- package/CHANGELOG.md +0 -250
- package/dist/chunk-MX2WL43P.js +0 -90
- package/dist/chunk-MX2WL43P.js.map +0 -1
- package/examples/pulsar-quickstart.ts +0 -35
- package/examples/resume-branch.ts +0 -45
- package/examples/subagent-fanout.ts +0 -20
- package/src/adapters/dispatcher-sse.test.ts +0 -218
- package/src/adapters/dispatcher-sse.ts +0 -222
- package/src/adapters/index.ts +0 -18
- package/src/adapters/mcp-catalog.test.ts +0 -213
- package/src/adapters/mcp-catalog.ts +0 -188
- package/src/adapters/prisma-rollout.test.ts +0 -153
- package/src/adapters/prisma-rollout.ts +0 -104
- package/src/agent-runtime.test.ts +0 -176
- package/src/artifact-stream.test.ts +0 -330
- package/src/artifact-stream.ts +0 -453
- package/src/channel-gateway.test.ts +0 -432
- package/src/channel-gateway.ts +0 -357
- package/src/cli.ts +0 -49
- package/src/code-review.test.ts +0 -501
- package/src/code-review.ts +0 -495
- package/src/command-suggestions.test.ts +0 -251
- package/src/command-suggestions.ts +0 -338
- package/src/commands.test.ts +0 -184
- package/src/commands.ts +0 -140
- package/src/commit-message.test.ts +0 -249
- package/src/commit-message.ts +0 -180
- package/src/context-compaction.test.ts +0 -522
- package/src/context-compaction.ts +0 -438
- package/src/definitions.test.ts +0 -78
- package/src/definitions.ts +0 -190
- package/src/deployment-status.test.ts +0 -215
- package/src/deployment-status.ts +0 -227
- package/src/design-context.test.ts +0 -195
- package/src/design-context.ts +0 -198
- package/src/dispatcher.test.ts +0 -234
- package/src/dispatcher.ts +0 -189
- package/src/durable-turn.test.ts +0 -209
- package/src/durable-turn.ts +0 -135
- package/src/edit-planner.test.ts +0 -204
- package/src/edit-planner.ts +0 -325
- package/src/fuzzy-match.test.ts +0 -311
- package/src/fuzzy-match.ts +0 -444
- package/src/hook-pipeline.test.ts +0 -279
- package/src/hook-pipeline.ts +0 -373
- package/src/inbound-admission.test.ts +0 -394
- package/src/inbound-admission.ts +0 -246
- package/src/index.ts +0 -49
- package/src/loop.test.ts +0 -161
- package/src/loop.ts +0 -211
- package/src/mcp-bridge.test.ts +0 -165
- package/src/mcp-bridge.ts +0 -81
- package/src/memory-provider.test.ts +0 -232
- package/src/memory-provider.ts +0 -257
- package/src/model.ts +0 -168
- package/src/orchestration.test.ts +0 -53
- package/src/orchestration.ts +0 -146
- package/src/permission-ruleset.test.ts +0 -301
- package/src/permission-ruleset.ts +0 -1
- package/src/policy.ts +0 -151
- package/src/project-repo.test.ts +0 -232
- package/src/project-repo.ts +0 -311
- package/src/protocol.ts +0 -159
- package/src/pulsar.test.ts +0 -156
- package/src/pulsar.ts +0 -322
- package/src/rollout-store-persistent.test.ts +0 -217
- package/src/rollout-store-persistent.ts +0 -166
- package/src/rollout.ts +0 -150
- package/src/sandbox.ts +0 -113
- package/src/session-share.test.ts +0 -360
- package/src/session-share.ts +0 -310
- package/src/skill-distillation.test.ts +0 -177
- package/src/skill-distillation.ts +0 -369
- package/src/skills.test.ts +0 -277
- package/src/skills.ts +0 -277
- package/src/subagents.test.ts +0 -290
- package/src/subagents.ts +0 -332
- package/src/tools.test.ts +0 -12
- package/src/tools.ts +0 -129
- package/src/workbench.test.ts +0 -0
- package/src/workbench.ts +0 -0
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -36
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Production-grade, tenant-scoped {@link RolloutStore} backed by an injectable
|
|
3
|
-
* persistence port. This package depends on NO datastore: a Postgres /
|
|
4
|
-
* ClickHouse / `@nebutra/db` / `@nebutra/audit` adapter satisfies
|
|
5
|
-
* {@link RolloutPersistencePort} from the outside, so the "no infra change"
|
|
6
|
-
* promise stays honest — swap the adapter, keep the interface.
|
|
7
|
-
*
|
|
8
|
-
* Tenancy is structural: `tenantId` is part of the storage key AND Zod-validated
|
|
9
|
-
* on every read and write. Cross-tenant access is impossible by construction.
|
|
10
|
-
* The store fails closed — empty identifiers and malformed stored payloads
|
|
11
|
-
* raise typed errors; records are never silently dropped.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { z } from "zod";
|
|
15
|
-
import type { RolloutLine, RolloutStore } from "./rollout";
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Minimal append-only persistence seam. Implementations MUST persist records
|
|
19
|
-
* durably and return them ascending by `seq` for a given (tenant, thread).
|
|
20
|
-
*/
|
|
21
|
-
export interface RolloutPersistencePort {
|
|
22
|
-
put(record: {
|
|
23
|
-
tenantId: string;
|
|
24
|
-
threadId: string;
|
|
25
|
-
seq: number;
|
|
26
|
-
at: string;
|
|
27
|
-
payload: string;
|
|
28
|
-
}): Promise<void>;
|
|
29
|
-
/** Records for one (tenant, thread), ascending by seq. */
|
|
30
|
-
list(tenantId: string, threadId: string): Promise<{ seq: number; payload: string }[]>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** Raised when a stored payload cannot be faithfully reconstructed. */
|
|
34
|
-
export class RoundTripError extends Error {
|
|
35
|
-
constructor(message: string, options?: { cause?: unknown }) {
|
|
36
|
-
super(message, options);
|
|
37
|
-
this.name = "RoundTripError";
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const tenantId = z.string().min(1, "tenantId is required");
|
|
42
|
-
const threadId = z.string().min(1, "threadId is required");
|
|
43
|
-
const at = z.string().min(1);
|
|
44
|
-
|
|
45
|
-
const turnConfigSchema = z.object({
|
|
46
|
-
model: z.string(),
|
|
47
|
-
provider: z.string(),
|
|
48
|
-
approvalPolicy: z.string(),
|
|
49
|
-
capabilityPolicy: z.string(),
|
|
50
|
-
reasoningEffort: z.enum(["low", "medium", "high"]).optional(),
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
const threadEventSchema = z.custom<RolloutLine extends { event: infer E } ? E : never>(
|
|
54
|
-
(v) => typeof v === "object" && v !== null && typeof (v as { type?: unknown }).type === "string",
|
|
55
|
-
{ message: "invalid ThreadEvent" },
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const rolloutLineSchema: z.ZodType<RolloutLine> = z.discriminatedUnion("type", [
|
|
59
|
-
z.object({
|
|
60
|
-
tenantId,
|
|
61
|
-
threadId,
|
|
62
|
-
type: z.literal("session_meta"),
|
|
63
|
-
config: turnConfigSchema,
|
|
64
|
-
at,
|
|
65
|
-
}),
|
|
66
|
-
z.object({
|
|
67
|
-
tenantId,
|
|
68
|
-
threadId,
|
|
69
|
-
type: z.literal("event"),
|
|
70
|
-
event: threadEventSchema,
|
|
71
|
-
at,
|
|
72
|
-
}),
|
|
73
|
-
z.object({
|
|
74
|
-
tenantId,
|
|
75
|
-
threadId,
|
|
76
|
-
type: z.literal("turn_context"),
|
|
77
|
-
config: turnConfigSchema,
|
|
78
|
-
at,
|
|
79
|
-
}),
|
|
80
|
-
z.object({
|
|
81
|
-
tenantId,
|
|
82
|
-
threadId,
|
|
83
|
-
type: z.literal("compacted"),
|
|
84
|
-
summary: z.string(),
|
|
85
|
-
droppedThrough: z.string(),
|
|
86
|
-
at,
|
|
87
|
-
}),
|
|
88
|
-
]) as z.ZodType<RolloutLine>;
|
|
89
|
-
|
|
90
|
-
const tenantThreadSchema = z.object({ tenantId, threadId });
|
|
91
|
-
|
|
92
|
-
export class PersistentRolloutStore implements RolloutStore {
|
|
93
|
-
readonly #port: RolloutPersistencePort;
|
|
94
|
-
/**
|
|
95
|
-
* Per-(tenant,thread) seq allocation chained through a promise so concurrent
|
|
96
|
-
* `append` calls to the same thread serialize and never collide on seq —
|
|
97
|
-
* without blocking appends to other threads.
|
|
98
|
-
*/
|
|
99
|
-
readonly #tails = new Map<string, Promise<number>>();
|
|
100
|
-
|
|
101
|
-
constructor(port: RolloutPersistencePort) {
|
|
102
|
-
this.#port = port;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
#key(t: string, th: string): string {
|
|
106
|
-
return `${t}::${th}`;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
async append(line: RolloutLine): Promise<void> {
|
|
110
|
-
const validated = rolloutLineSchema.parse(line);
|
|
111
|
-
const key = this.#key(validated.tenantId, validated.threadId);
|
|
112
|
-
|
|
113
|
-
const prev = this.#tails.get(key) ?? Promise.resolve(-1);
|
|
114
|
-
const next = prev.then(async (last) => {
|
|
115
|
-
const seq = last + 1;
|
|
116
|
-
await this.#port.put({
|
|
117
|
-
tenantId: validated.tenantId,
|
|
118
|
-
threadId: validated.threadId,
|
|
119
|
-
seq,
|
|
120
|
-
at: validated.at,
|
|
121
|
-
payload: JSON.stringify(validated),
|
|
122
|
-
});
|
|
123
|
-
return seq;
|
|
124
|
-
});
|
|
125
|
-
// Keep the chain advancing even if a put fails (fail-closed for the caller,
|
|
126
|
-
// but the next append must not deadlock on a rejected tail).
|
|
127
|
-
this.#tails.set(
|
|
128
|
-
key,
|
|
129
|
-
next.catch(() => (this.#tails.get(key) === next ? -1 : next)),
|
|
130
|
-
);
|
|
131
|
-
await next;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
async read(tenantIdArg: string, threadIdArg: string): Promise<readonly RolloutLine[]> {
|
|
135
|
-
const { tenantId: t, threadId: th } = tenantThreadSchema.parse({
|
|
136
|
-
tenantId: tenantIdArg,
|
|
137
|
-
threadId: threadIdArg,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const records = await this.#port.list(t, th);
|
|
141
|
-
const ordered = [...records].sort((a, b) => a.seq - b.seq);
|
|
142
|
-
|
|
143
|
-
return ordered.map((r) => {
|
|
144
|
-
let parsed: unknown;
|
|
145
|
-
try {
|
|
146
|
-
parsed = JSON.parse(r.payload);
|
|
147
|
-
} catch (cause) {
|
|
148
|
-
throw new RoundTripError(`malformed stored payload at seq ${r.seq} for ${t}::${th}`, {
|
|
149
|
-
cause,
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
const result = rolloutLineSchema.safeParse(parsed);
|
|
153
|
-
if (!result.success) {
|
|
154
|
-
throw new RoundTripError(
|
|
155
|
-
`structurally-invalid stored payload at seq ${r.seq} for ${t}::${th}`,
|
|
156
|
-
{ cause: result.error },
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
const reconstructed = result.data;
|
|
160
|
-
if (reconstructed.tenantId !== t || reconstructed.threadId !== th) {
|
|
161
|
-
throw new RoundTripError(`tenant/thread mismatch at seq ${r.seq} for ${t}::${th}`);
|
|
162
|
-
}
|
|
163
|
-
return reconstructed;
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
}
|
package/src/rollout.ts
DELETED
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Event-sourced rollout / session trace (WRAP — capability #7).
|
|
3
|
-
*
|
|
4
|
-
* Faithful re-expression of the upstream rollout model: an append-only typed
|
|
5
|
-
* event log, state derived by replay, a first-class context-compaction marker
|
|
6
|
-
* (so replay stays bounded), and a per-item persistence policy that sanitizes
|
|
7
|
-
* and size-caps outputs before they are durable.
|
|
8
|
-
*
|
|
9
|
-
* Multi-tenant adaptation: every rollout line carries `tenantId`; the store
|
|
10
|
-
* interface is tenant-scoped so a Postgres/ClickHouse-backed implementation
|
|
11
|
-
* (via `@nebutra/audit` providers / `@nebutra/db`) replaces the upstream
|
|
12
|
-
* single-host jsonl + embedded-SQLite assumptions. Only an in-memory
|
|
13
|
-
* reference store ships here.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import type { ThreadEvent, ThreadItem, TurnConfig } from "./model";
|
|
17
|
-
|
|
18
|
-
/** Cap mirrors the upstream persisted-output ceiling. */
|
|
19
|
-
export const PERSISTED_OUTPUT_MAX_BYTES = 10_000;
|
|
20
|
-
|
|
21
|
-
export type EventPersistenceMode = "limited" | "extended";
|
|
22
|
-
|
|
23
|
-
/** One durable line in a thread's append-only log. */
|
|
24
|
-
export type RolloutLine =
|
|
25
|
-
| {
|
|
26
|
-
readonly tenantId: string;
|
|
27
|
-
readonly threadId: string;
|
|
28
|
-
readonly type: "session_meta";
|
|
29
|
-
readonly config: TurnConfig;
|
|
30
|
-
readonly at: string;
|
|
31
|
-
}
|
|
32
|
-
| {
|
|
33
|
-
readonly tenantId: string;
|
|
34
|
-
readonly threadId: string;
|
|
35
|
-
readonly type: "event";
|
|
36
|
-
readonly event: ThreadEvent;
|
|
37
|
-
readonly at: string;
|
|
38
|
-
}
|
|
39
|
-
| {
|
|
40
|
-
readonly tenantId: string;
|
|
41
|
-
readonly threadId: string;
|
|
42
|
-
readonly type: "turn_context";
|
|
43
|
-
readonly config: TurnConfig;
|
|
44
|
-
readonly at: string;
|
|
45
|
-
}
|
|
46
|
-
| {
|
|
47
|
-
readonly tenantId: string;
|
|
48
|
-
readonly threadId: string;
|
|
49
|
-
readonly type: "compacted";
|
|
50
|
-
readonly summary: string;
|
|
51
|
-
readonly droppedThrough: string;
|
|
52
|
-
readonly at: string;
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Persistence policy: decide per item whether it is durable, and sanitize +
|
|
57
|
-
* size-cap its output first. Executive markers are always persisted.
|
|
58
|
-
*/
|
|
59
|
-
export function isPersisted(item: ThreadItem, mode: EventPersistenceMode): boolean {
|
|
60
|
-
if (item.type === "reasoning") return mode === "extended";
|
|
61
|
-
return true;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/** Truncate aggregated command output to the persisted ceiling. */
|
|
65
|
-
export function sanitizeForPersist(item: ThreadItem): ThreadItem {
|
|
66
|
-
if (item.type !== "command_execution") return item;
|
|
67
|
-
const bytes = Buffer.byteLength(item.aggregatedOutput, "utf8");
|
|
68
|
-
if (bytes <= PERSISTED_OUTPUT_MAX_BYTES) return item;
|
|
69
|
-
const truncated = Buffer.from(item.aggregatedOutput, "utf8")
|
|
70
|
-
.subarray(0, PERSISTED_OUTPUT_MAX_BYTES)
|
|
71
|
-
.toString("utf8");
|
|
72
|
-
return { ...item, aggregatedOutput: `${truncated}\n…[truncated]` };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/** Tenant-scoped append-only store. Replace the impl, keep the interface. */
|
|
76
|
-
export interface RolloutStore {
|
|
77
|
-
append(line: RolloutLine): Promise<void>;
|
|
78
|
-
/** Lines for a thread, in append order. */
|
|
79
|
-
read(tenantId: string, threadId: string): Promise<readonly RolloutLine[]>;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** In-memory reference store (dev/test only — never multi-instance safe). */
|
|
83
|
-
export class InMemoryRolloutStore implements RolloutStore {
|
|
84
|
-
readonly #lines = new Map<string, RolloutLine[]>();
|
|
85
|
-
|
|
86
|
-
#key(tenantId: string, threadId: string): string {
|
|
87
|
-
return `${tenantId}::${threadId}`;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
async append(line: RolloutLine): Promise<void> {
|
|
91
|
-
const key = this.#key(line.tenantId, line.threadId);
|
|
92
|
-
const list = this.#lines.get(key) ?? [];
|
|
93
|
-
list.push(line);
|
|
94
|
-
this.#lines.set(key, list);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
async read(tenantId: string, threadId: string): Promise<readonly RolloutLine[]> {
|
|
98
|
-
return [...(this.#lines.get(this.#key(tenantId, threadId)) ?? [])];
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
/** Derived thread state — the projection rebuilt by replaying the log. */
|
|
103
|
-
export interface ThreadProjection {
|
|
104
|
-
readonly tenantId: string;
|
|
105
|
-
readonly threadId: string;
|
|
106
|
-
config?: TurnConfig;
|
|
107
|
-
readonly items: ThreadItem[];
|
|
108
|
-
/** Marks where a compaction summary replaced earlier history. */
|
|
109
|
-
compactionSummary?: string;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Replay an append-only log into derived state. A `compacted` line bounds
|
|
114
|
-
* replay: items prior to it are represented by the summary, not re-applied.
|
|
115
|
-
*/
|
|
116
|
-
export function replay(lines: readonly RolloutLine[]): ThreadProjection | null {
|
|
117
|
-
const first = lines[0];
|
|
118
|
-
if (!first) return null;
|
|
119
|
-
const projection: ThreadProjection = {
|
|
120
|
-
tenantId: first.tenantId,
|
|
121
|
-
threadId: first.threadId,
|
|
122
|
-
items: [],
|
|
123
|
-
};
|
|
124
|
-
for (const line of lines) {
|
|
125
|
-
switch (line.type) {
|
|
126
|
-
case "session_meta":
|
|
127
|
-
case "turn_context":
|
|
128
|
-
projection.config = line.config;
|
|
129
|
-
break;
|
|
130
|
-
case "compacted":
|
|
131
|
-
projection.compactionSummary = line.summary;
|
|
132
|
-
projection.items.length = 0;
|
|
133
|
-
break;
|
|
134
|
-
case "event": {
|
|
135
|
-
const e = line.event;
|
|
136
|
-
if (e.type === "item.completed" || e.type === "item.started") {
|
|
137
|
-
const idx = projection.items.findIndex((i) => i.id === e.item.id);
|
|
138
|
-
if (idx >= 0) projection.items[idx] = e.item;
|
|
139
|
-
else projection.items.push(e.item);
|
|
140
|
-
} else if (e.type === "item.updated") {
|
|
141
|
-
const idx = projection.items.findIndex((i) => i.id === e.item.id);
|
|
142
|
-
if (idx >= 0) projection.items[idx] = e.item;
|
|
143
|
-
else projection.items.push(e.item);
|
|
144
|
-
}
|
|
145
|
-
break;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return projection;
|
|
150
|
-
}
|
package/src/sandbox.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* External-sandbox delegation seam (PORT — capability #11).
|
|
3
|
-
*
|
|
4
|
-
* Governance decision (dual-track, ExternalSandbox posture): this multi-tenant
|
|
5
|
-
* web runtime NEVER executes untrusted code itself and introduces no isolation
|
|
6
|
-
* infrastructure. It only:
|
|
7
|
-
* 1. carries the capability policy (see ./policy),
|
|
8
|
-
* 2. delegates execution to an external isolator behind this interface
|
|
9
|
-
* (a self-hosted kernel sidecar — Track B — or any other executor),
|
|
10
|
-
* 3. records the outcome as a `command_execution` item (see ./model).
|
|
11
|
-
*
|
|
12
|
-
* The upstream OS enforcers (Seatbelt / Landlock / bubblewrap / Windows
|
|
13
|
-
* restricted token) are deliberately NOT ported — they are single-host and
|
|
14
|
-
* out of scope for a multi-tenant web product.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import type { CapabilityPolicy } from "./policy";
|
|
18
|
-
|
|
19
|
-
export interface SandboxExecRequest {
|
|
20
|
-
/** Mandatory tenant scope — every delegated exec is tenant-bound. */
|
|
21
|
-
readonly tenantId: string;
|
|
22
|
-
readonly threadId: string;
|
|
23
|
-
readonly command: string;
|
|
24
|
-
/** The capabilities the external isolator must honor. */
|
|
25
|
-
readonly capabilityPolicy: CapabilityPolicy;
|
|
26
|
-
/** Optional inputs the executor should materialize before running. */
|
|
27
|
-
readonly inputs?: Readonly<Record<string, string>>;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface SandboxExecResult {
|
|
31
|
-
readonly exitCode: number;
|
|
32
|
-
readonly aggregatedOutput: string;
|
|
33
|
-
/** Identifier of the isolator that actually ran the command. */
|
|
34
|
-
readonly executedOn: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* The only thing Track A depends on for execution. Implemented by a decoupled
|
|
39
|
-
* isolator over the ./protocol contract; never implemented in-process here.
|
|
40
|
-
*/
|
|
41
|
-
export interface ExternalSandbox {
|
|
42
|
-
exec(request: SandboxExecRequest): Promise<SandboxExecResult>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Fail-closed default. Wiring this in production requires a real
|
|
47
|
-
* {@link ExternalSandbox}; absent one, untrusted code never runs — by design.
|
|
48
|
-
*/
|
|
49
|
-
export class NoExecutorConfiguredError extends Error {
|
|
50
|
-
constructor() {
|
|
51
|
-
super(
|
|
52
|
-
"No ExternalSandbox configured. This runtime never executes untrusted " +
|
|
53
|
-
"code in-process; delegate to a decoupled isolator (Track B).",
|
|
54
|
-
);
|
|
55
|
-
this.name = "NoExecutorConfiguredError";
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export const REFUSING_SANDBOX: ExternalSandbox = {
|
|
60
|
-
async exec(): Promise<SandboxExecResult> {
|
|
61
|
-
throw new NoExecutorConfiguredError();
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export class SandboxDelegationError extends Error {
|
|
66
|
-
constructor(
|
|
67
|
-
message: string,
|
|
68
|
-
readonly status: number,
|
|
69
|
-
) {
|
|
70
|
-
super(message);
|
|
71
|
-
this.name = "SandboxDelegationError";
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The concrete Track-B coupling: delegate execution over HTTP to the decoupled
|
|
77
|
-
* Rust isolator (`backends/rust/sandbox`, `POST /api/v1/sandbox/exec`). The
|
|
78
|
-
* isolator is fail-closed; a non-2xx (e.g. 403 refusal) is surfaced as an
|
|
79
|
-
* error and never coerced into a fabricated result.
|
|
80
|
-
*/
|
|
81
|
-
export function createHttpSandbox(
|
|
82
|
-
baseUrl: string,
|
|
83
|
-
fetchImpl: typeof fetch = fetch,
|
|
84
|
-
): ExternalSandbox {
|
|
85
|
-
const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/sandbox/exec`;
|
|
86
|
-
return {
|
|
87
|
-
async exec(request: SandboxExecRequest): Promise<SandboxExecResult> {
|
|
88
|
-
const response = await fetchImpl(endpoint, {
|
|
89
|
-
method: "POST",
|
|
90
|
-
headers: { "content-type": "application/json" },
|
|
91
|
-
body: JSON.stringify(request),
|
|
92
|
-
});
|
|
93
|
-
if (!response.ok) {
|
|
94
|
-
const detail = await response.text().catch(() => "");
|
|
95
|
-
throw new SandboxDelegationError(
|
|
96
|
-
`Isolator refused execution (${response.status}): ${detail}`,
|
|
97
|
-
response.status,
|
|
98
|
-
);
|
|
99
|
-
}
|
|
100
|
-
return (await response.json()) as SandboxExecResult;
|
|
101
|
-
},
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/** Guard: reject the most dangerous posture unless explicitly opted in. */
|
|
106
|
-
export function assertSafePosture(policy: CapabilityPolicy, allowDanger = false): void {
|
|
107
|
-
if (policy.kind === "danger_full_access" && !allowDanger) {
|
|
108
|
-
throw new Error(
|
|
109
|
-
"danger_full_access capability policy refused: not permitted for " +
|
|
110
|
-
"multi-tenant delegation without explicit opt-in.",
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
}
|