@nebutra/agent-runtime 0.2.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/.turbo/turbo-build.log +115 -0
- package/.turbo/turbo-test.log +44 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +253 -0
- package/LICENSE +676 -0
- package/README.md +50 -0
- package/dist/adapters/dispatcher-sse.d.ts +68 -0
- package/dist/adapters/dispatcher-sse.js +11 -0
- package/dist/adapters/dispatcher-sse.js.map +1 -0
- package/dist/adapters/index.d.ts +12 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mcp-catalog.d.ts +58 -0
- package/dist/adapters/mcp-catalog.js +9 -0
- package/dist/adapters/mcp-catalog.js.map +1 -0
- package/dist/adapters/prisma-rollout.d.ts +60 -0
- package/dist/adapters/prisma-rollout.js +7 -0
- package/dist/adapters/prisma-rollout.js.map +1 -0
- package/dist/chunk-24ZXP7FI.js +93 -0
- package/dist/chunk-24ZXP7FI.js.map +1 -0
- package/dist/chunk-2DA6Q6TN.js +126 -0
- package/dist/chunk-2DA6Q6TN.js.map +1 -0
- package/dist/chunk-37BBB2P2.js +73 -0
- package/dist/chunk-37BBB2P2.js.map +1 -0
- package/dist/chunk-57W3AR43.js +52 -0
- package/dist/chunk-57W3AR43.js.map +1 -0
- package/dist/chunk-5N4644PB.js +67 -0
- package/dist/chunk-5N4644PB.js.map +1 -0
- package/dist/chunk-5YS7WAPS.js +177 -0
- package/dist/chunk-5YS7WAPS.js.map +1 -0
- package/dist/chunk-6EGG2OZC.js +13 -0
- package/dist/chunk-6EGG2OZC.js.map +1 -0
- package/dist/chunk-7BUOF367.js +126 -0
- package/dist/chunk-7BUOF367.js.map +1 -0
- package/dist/chunk-BJBBR3QA.js +121 -0
- package/dist/chunk-BJBBR3QA.js.map +1 -0
- package/dist/chunk-CGRCUKGT.js +73 -0
- package/dist/chunk-CGRCUKGT.js.map +1 -0
- package/dist/chunk-FUG5DT2C.js +75 -0
- package/dist/chunk-FUG5DT2C.js.map +1 -0
- package/dist/chunk-LO24VOA3.js +199 -0
- package/dist/chunk-LO24VOA3.js.map +1 -0
- package/dist/chunk-MUF7ZZTO.js +57 -0
- package/dist/chunk-MUF7ZZTO.js.map +1 -0
- package/dist/chunk-NN7DATXA.js +46 -0
- package/dist/chunk-NN7DATXA.js.map +1 -0
- package/dist/chunk-PGGWSUTM.js +33 -0
- package/dist/chunk-PGGWSUTM.js.map +1 -0
- package/dist/chunk-RDKYDMXT.js +135 -0
- package/dist/chunk-RDKYDMXT.js.map +1 -0
- package/dist/chunk-YYFPDBJG.js +63 -0
- package/dist/chunk-YYFPDBJG.js.map +1 -0
- package/dist/chunk-ZMYX5VBU.js +135 -0
- package/dist/chunk-ZMYX5VBU.js.map +1 -0
- package/dist/chunk-ZTSKS42I.js +131 -0
- package/dist/chunk-ZTSKS42I.js.map +1 -0
- package/dist/commands.d.ts +74 -0
- package/dist/commands.js +10 -0
- package/dist/commands.js.map +1 -0
- package/dist/definitions.d.ts +94 -0
- package/dist/definitions.js +15 -0
- package/dist/definitions.js.map +1 -0
- package/dist/dispatcher.d.ts +50 -0
- package/dist/dispatcher.js +8 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/durable-turn.d.ts +58 -0
- package/dist/durable-turn.js +9 -0
- package/dist/durable-turn.js.map +1 -0
- package/dist/hook-pipeline.d.ts +114 -0
- package/dist/hook-pipeline.js +13 -0
- package/dist/hook-pipeline.js.map +1 -0
- package/dist/index.d.ts +1874 -0
- package/dist/index.js +3117 -0
- package/dist/index.js.map +1 -0
- package/dist/loop.d.ts +78 -0
- package/dist/loop.js +9 -0
- package/dist/loop.js.map +1 -0
- package/dist/mcp-bridge.d.ts +48 -0
- package/dist/mcp-bridge.js +8 -0
- package/dist/mcp-bridge.js.map +1 -0
- package/dist/model.d.ts +154 -0
- package/dist/model.js +9 -0
- package/dist/model.js.map +1 -0
- package/dist/policy.d.ts +130 -0
- package/dist/policy.js +23 -0
- package/dist/policy.js.map +1 -0
- package/dist/protocol.d.ts +170 -0
- package/dist/protocol.js +15 -0
- package/dist/protocol.js.map +1 -0
- package/dist/rollout-store-persistent.d.ts +48 -0
- package/dist/rollout-store-persistent.js +9 -0
- package/dist/rollout-store-persistent.js.map +1 -0
- package/dist/rollout.d.ts +82 -0
- package/dist/rollout.js +15 -0
- package/dist/rollout.js.map +1 -0
- package/dist/sandbox.d.ts +65 -0
- package/dist/sandbox.js +15 -0
- package/dist/sandbox.js.map +1 -0
- package/dist/skills.d.ts +93 -0
- package/dist/skills.js +10 -0
- package/dist/skills.js.map +1 -0
- package/dist/subagents.d.ts +129 -0
- package/dist/subagents.js +21 -0
- package/dist/subagents.js.map +1 -0
- package/dist/tools.d.ts +77 -0
- package/dist/tools.js +9 -0
- package/dist/tools.js.map +1 -0
- package/package.json +74 -0
- package/src/adapters/dispatcher-sse.test.ts +218 -0
- package/src/adapters/dispatcher-sse.ts +222 -0
- package/src/adapters/index.ts +18 -0
- package/src/adapters/mcp-catalog.test.ts +213 -0
- package/src/adapters/mcp-catalog.ts +188 -0
- package/src/adapters/prisma-rollout.test.ts +153 -0
- package/src/adapters/prisma-rollout.ts +104 -0
- package/src/agent-runtime.test.ts +176 -0
- package/src/artifact-stream.test.ts +330 -0
- package/src/artifact-stream.ts +453 -0
- package/src/channel-gateway.test.ts +432 -0
- package/src/channel-gateway.ts +357 -0
- package/src/code-review.test.ts +501 -0
- package/src/code-review.ts +495 -0
- package/src/command-suggestions.test.ts +251 -0
- package/src/command-suggestions.ts +338 -0
- package/src/commands.test.ts +184 -0
- package/src/commands.ts +140 -0
- package/src/commit-message.test.ts +249 -0
- package/src/commit-message.ts +180 -0
- package/src/context-compaction.test.ts +522 -0
- package/src/context-compaction.ts +434 -0
- package/src/definitions.test.ts +78 -0
- package/src/definitions.ts +190 -0
- package/src/deployment-status.test.ts +215 -0
- package/src/deployment-status.ts +227 -0
- package/src/design-context.test.ts +195 -0
- package/src/design-context.ts +198 -0
- package/src/dispatcher.test.ts +234 -0
- package/src/dispatcher.ts +189 -0
- package/src/durable-turn.test.ts +209 -0
- package/src/durable-turn.ts +135 -0
- package/src/edit-planner.test.ts +204 -0
- package/src/edit-planner.ts +325 -0
- package/src/fuzzy-match.test.ts +311 -0
- package/src/fuzzy-match.ts +444 -0
- package/src/hook-pipeline.test.ts +279 -0
- package/src/hook-pipeline.ts +373 -0
- package/src/inbound-admission.test.ts +394 -0
- package/src/inbound-admission.ts +246 -0
- package/src/index.ts +47 -0
- package/src/loop.test.ts +161 -0
- package/src/loop.ts +211 -0
- package/src/mcp-bridge.test.ts +165 -0
- package/src/mcp-bridge.ts +76 -0
- package/src/memory-provider.test.ts +232 -0
- package/src/memory-provider.ts +257 -0
- package/src/model.ts +168 -0
- package/src/permission-ruleset.test.ts +301 -0
- package/src/permission-ruleset.ts +200 -0
- package/src/policy.ts +151 -0
- package/src/project-repo.test.ts +232 -0
- package/src/project-repo.ts +311 -0
- package/src/protocol.ts +159 -0
- package/src/rollout-store-persistent.test.ts +217 -0
- package/src/rollout-store-persistent.ts +166 -0
- package/src/rollout.ts +150 -0
- package/src/sandbox.ts +113 -0
- package/src/session-share.test.ts +360 -0
- package/src/session-share.ts +310 -0
- package/src/skill-distillation.test.ts +177 -0
- package/src/skill-distillation.ts +369 -0
- package/src/skills.test.ts +277 -0
- package/src/skills.ts +255 -0
- package/src/subagents.test.ts +290 -0
- package/src/subagents.ts +332 -0
- package/src/tools.ts +126 -0
- package/src/workbench.test.ts +0 -0
- package/src/workbench.ts +0 -0
- package/tsconfig.json +12 -0
- package/tsup.config.ts +33 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DENIED,
|
|
3
|
+
isApproval,
|
|
4
|
+
resolveRuleDecision
|
|
5
|
+
} from "./chunk-CGRCUKGT.js";
|
|
6
|
+
import {
|
|
7
|
+
sanitizeForPersist
|
|
8
|
+
} from "./chunk-37BBB2P2.js";
|
|
9
|
+
|
|
10
|
+
// src/loop.ts
|
|
11
|
+
var DEFAULT_MAX_STEPS = 20;
|
|
12
|
+
var requirePrompt = () => "prompt";
|
|
13
|
+
function newId(prefix) {
|
|
14
|
+
return `${prefix}_${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`}`;
|
|
15
|
+
}
|
|
16
|
+
async function* runTurn(userInput, deps) {
|
|
17
|
+
const at = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
18
|
+
const ruleEvaluator = deps.ruleEvaluator ?? requirePrompt;
|
|
19
|
+
const maxSteps = deps.maxSteps ?? DEFAULT_MAX_STEPS;
|
|
20
|
+
const ctx = { tenantId: deps.tenantId, threadId: deps.threadId };
|
|
21
|
+
const append = async (line) => deps.store.append(line);
|
|
22
|
+
const event = async (e) => {
|
|
23
|
+
await append({
|
|
24
|
+
tenantId: deps.tenantId,
|
|
25
|
+
threadId: deps.threadId,
|
|
26
|
+
type: "event",
|
|
27
|
+
event: e.type === "item.completed" ? { type: "item.completed", item: sanitizeForPersist(e.item) } : e,
|
|
28
|
+
at: at()
|
|
29
|
+
});
|
|
30
|
+
return e;
|
|
31
|
+
};
|
|
32
|
+
yield await event({ type: "turn.started" });
|
|
33
|
+
const history = [{ role: "user", content: userInput }];
|
|
34
|
+
let usage = {
|
|
35
|
+
inputTokens: 0,
|
|
36
|
+
cachedInputTokens: 0,
|
|
37
|
+
outputTokens: 0,
|
|
38
|
+
reasoningOutputTokens: 0
|
|
39
|
+
};
|
|
40
|
+
try {
|
|
41
|
+
for (let step = 0; step < maxSteps; step++) {
|
|
42
|
+
const round = await deps.model.invoke({
|
|
43
|
+
config: deps.config,
|
|
44
|
+
history,
|
|
45
|
+
toolNames: deps.tools.list().map((t) => t.definition.name)
|
|
46
|
+
});
|
|
47
|
+
if (round.usage) {
|
|
48
|
+
const u = round.usage;
|
|
49
|
+
usage = {
|
|
50
|
+
inputTokens: usage.inputTokens + (u.inputTokens ?? 0),
|
|
51
|
+
cachedInputTokens: usage.cachedInputTokens + (u.cachedInputTokens ?? 0),
|
|
52
|
+
outputTokens: usage.outputTokens + (u.outputTokens ?? 0),
|
|
53
|
+
reasoningOutputTokens: usage.reasoningOutputTokens + (u.reasoningOutputTokens ?? 0)
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const toolCalls = round.emissions.filter(
|
|
57
|
+
(e) => e.kind === "tool_call"
|
|
58
|
+
);
|
|
59
|
+
for (const e of round.emissions) {
|
|
60
|
+
if (e.kind !== "text") continue;
|
|
61
|
+
const item = { id: newId("msg"), type: "agent_message", text: e.text };
|
|
62
|
+
history.push({ role: "assistant", content: e.text });
|
|
63
|
+
yield await event({ type: "item.completed", item });
|
|
64
|
+
}
|
|
65
|
+
if (toolCalls.length === 0) break;
|
|
66
|
+
for (const call of toolCalls) {
|
|
67
|
+
const decision = await gateToolCall(call.name, call.args, deps, ruleEvaluator);
|
|
68
|
+
if (!isApproval(decision)) {
|
|
69
|
+
const failed = {
|
|
70
|
+
id: call.id,
|
|
71
|
+
type: "error",
|
|
72
|
+
message: `tool '${call.name}' not approved (${decision.kind})`
|
|
73
|
+
};
|
|
74
|
+
history.push({ role: "tool", content: `DENIED: ${call.name}` });
|
|
75
|
+
yield await event({ type: "item.completed", item: failed });
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
const output = await deps.tools.dispatch(call.name, call.args, ctx);
|
|
80
|
+
const item = {
|
|
81
|
+
id: call.id,
|
|
82
|
+
type: "mcp_tool_call",
|
|
83
|
+
server: "native",
|
|
84
|
+
tool: call.name,
|
|
85
|
+
arguments: call.args,
|
|
86
|
+
result: { content: output },
|
|
87
|
+
status: "completed"
|
|
88
|
+
};
|
|
89
|
+
history.push({ role: "tool", content: JSON.stringify(output) });
|
|
90
|
+
yield await event({ type: "item.completed", item });
|
|
91
|
+
} catch (err) {
|
|
92
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
93
|
+
history.push({ role: "tool", content: `ERROR: ${message}` });
|
|
94
|
+
yield await event({
|
|
95
|
+
type: "item.completed",
|
|
96
|
+
item: { id: call.id, type: "error", message }
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
yield await event({ type: "turn.completed", usage });
|
|
102
|
+
} catch (err) {
|
|
103
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
104
|
+
yield await event({ type: "turn.failed", error: { message } });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function gateToolCall(name, args, deps, ruleEvaluator) {
|
|
108
|
+
const outcome = resolveRuleDecision(ruleEvaluator(name, args), deps.approvalPolicy);
|
|
109
|
+
if (outcome === "auto_allow") return { kind: "approved" };
|
|
110
|
+
if (outcome === "auto_reject") return DENIED;
|
|
111
|
+
return deps.approvalGate.request({
|
|
112
|
+
type: "permissions.request_approval",
|
|
113
|
+
requestId: newId("appr"),
|
|
114
|
+
summary: `tool '${name}'`
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
runTurn
|
|
120
|
+
};
|
|
121
|
+
//# sourceMappingURL=chunk-BJBBR3QA.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/loop.ts"],"sourcesContent":["/**\n * Agent loop runner (WRAP — the turn engine).\n *\n * Faithful re-expression of the upstream loop: a turn is\n * `loop { model_call → emit items → execute tools → feed results back }`\n * until the model stops requesting tools or a bounded step ceiling is hit.\n * Single-threaded (Cognition teaching: shared context, no conflicting\n * sub-agent decisions). Every item is appended to the tenant-scoped rollout\n * as it reaches a terminal state, so the turn is resumable by replay.\n *\n * The model call is abstracted behind {@link ModelInvoker} so this WRAPs an\n * existing model stack (e.g. `@nebutra/agents`) rather than re-porting\n * provider/routing/fallback. No untrusted code runs here — command items are\n * dispatched through the tool registry / external-sandbox seam.\n */\n\nimport type { AgentMessageItem, ThreadEvent, ThreadItem, TurnConfig, TurnUsage } from \"./model\";\nimport {\n type ApprovalPolicy,\n DENIED,\n isApproval,\n type ReviewDecision,\n type RuleDecision,\n resolveRuleDecision,\n} from \"./policy\";\nimport type { ServerRequest } from \"./protocol\";\nimport { type RolloutLine, type RolloutStore, sanitizeForPersist } from \"./rollout\";\nimport type { ToolDispatchContext, ToolRegistry } from \"./tools\";\n\n/** A single thing the model emitted in one round. */\nexport type ModelEmission =\n | { readonly kind: \"text\"; readonly text: string }\n | {\n readonly kind: \"tool_call\";\n readonly id: string;\n readonly name: string;\n readonly args: unknown;\n };\n\nexport interface ModelRoundResult {\n readonly emissions: readonly ModelEmission[];\n readonly usage?: Partial<TurnUsage>;\n}\n\nexport interface ModelRoundRequest {\n readonly config: TurnConfig;\n /** Running transcript: user input, agent text, and tool results. */\n readonly history: readonly { readonly role: string; readonly content: string }[];\n readonly toolNames: readonly string[];\n}\n\n/** Abstracts the model stack — implement over `@nebutra/agents`, etc. */\nexport interface ModelInvoker {\n invoke(request: ModelRoundRequest): Promise<ModelRoundResult>;\n}\n\n/** Server-initiated approval transport (see {@link ServerRequest}). */\nexport interface ApprovalGate {\n request(serverRequest: ServerRequest): Promise<ReviewDecision>;\n}\n\n/** Classifies a tool call into a static rule decision before approval. */\nexport type RuleEvaluator = (toolName: string, args: unknown) => RuleDecision;\n\nexport interface RunTurnDeps {\n readonly tenantId: string;\n readonly threadId: string;\n readonly config: TurnConfig;\n readonly approvalPolicy: ApprovalPolicy;\n readonly model: ModelInvoker;\n readonly tools: ToolRegistry;\n readonly store: RolloutStore;\n readonly approvalGate: ApprovalGate;\n /** Defaults to: everything requires a prompt (safe). */\n readonly ruleEvaluator?: RuleEvaluator;\n /** Bounded steps (parity with upstream step ceiling). Default 20. */\n readonly maxSteps?: number;\n}\n\nconst DEFAULT_MAX_STEPS = 20;\nconst requirePrompt: RuleEvaluator = () => \"prompt\";\n\nfunction newId(prefix: string): string {\n return `${prefix}_${globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`}`;\n}\n\n/**\n * Drive one turn to completion. Yields the {@link ThreadEvent} stream and\n * appends each terminal item + the turn outcome to the rollout store.\n * Never throws to the caller — failures surface as a `turn.failed` event.\n */\nexport async function* runTurn(userInput: string, deps: RunTurnDeps): AsyncGenerator<ThreadEvent> {\n const at = () => new Date().toISOString();\n const ruleEvaluator = deps.ruleEvaluator ?? requirePrompt;\n const maxSteps = deps.maxSteps ?? DEFAULT_MAX_STEPS;\n const ctx: ToolDispatchContext = { tenantId: deps.tenantId, threadId: deps.threadId };\n\n const append = async (line: RolloutLine): Promise<void> => deps.store.append(line);\n const event = async (e: ThreadEvent): Promise<ThreadEvent> => {\n await append({\n tenantId: deps.tenantId,\n threadId: deps.threadId,\n type: \"event\",\n event:\n e.type === \"item.completed\"\n ? { type: \"item.completed\", item: sanitizeForPersist(e.item) }\n : e,\n at: at(),\n });\n return e;\n };\n\n yield await event({ type: \"turn.started\" });\n\n const history: { role: string; content: string }[] = [{ role: \"user\", content: userInput }];\n let usage: TurnUsage = {\n inputTokens: 0,\n cachedInputTokens: 0,\n outputTokens: 0,\n reasoningOutputTokens: 0,\n };\n\n try {\n for (let step = 0; step < maxSteps; step++) {\n const round = await deps.model.invoke({\n config: deps.config,\n history,\n toolNames: deps.tools.list().map((t) => t.definition.name),\n });\n if (round.usage) {\n const u = round.usage;\n usage = {\n inputTokens: usage.inputTokens + (u.inputTokens ?? 0),\n cachedInputTokens: usage.cachedInputTokens + (u.cachedInputTokens ?? 0),\n outputTokens: usage.outputTokens + (u.outputTokens ?? 0),\n reasoningOutputTokens: usage.reasoningOutputTokens + (u.reasoningOutputTokens ?? 0),\n };\n }\n\n const toolCalls = round.emissions.filter(\n (e): e is Extract<ModelEmission, { kind: \"tool_call\" }> => e.kind === \"tool_call\",\n );\n\n for (const e of round.emissions) {\n if (e.kind !== \"text\") continue;\n const item: AgentMessageItem = { id: newId(\"msg\"), type: \"agent_message\", text: e.text };\n history.push({ role: \"assistant\", content: e.text });\n yield await event({ type: \"item.completed\", item });\n }\n\n if (toolCalls.length === 0) break; // model is done\n\n for (const call of toolCalls) {\n const decision = await gateToolCall(call.name, call.args, deps, ruleEvaluator);\n if (!isApproval(decision)) {\n const failed: ThreadItem = {\n id: call.id,\n type: \"error\",\n message: `tool '${call.name}' not approved (${decision.kind})`,\n };\n history.push({ role: \"tool\", content: `DENIED: ${call.name}` });\n yield await event({ type: \"item.completed\", item: failed });\n continue;\n }\n try {\n const output = await deps.tools.dispatch(call.name, call.args, ctx);\n const item: ThreadItem = {\n id: call.id,\n type: \"mcp_tool_call\",\n server: \"native\",\n tool: call.name,\n arguments: call.args,\n result: { content: output },\n status: \"completed\",\n };\n history.push({ role: \"tool\", content: JSON.stringify(output) });\n yield await event({ type: \"item.completed\", item });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n history.push({ role: \"tool\", content: `ERROR: ${message}` });\n yield await event({\n type: \"item.completed\",\n item: { id: call.id, type: \"error\", message },\n });\n }\n }\n }\n\n yield await event({ type: \"turn.completed\", usage });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n yield await event({ type: \"turn.failed\", error: { message } });\n }\n}\n\n/** Resolve a tool call's approval, raising a server-initiated request if asked. */\nasync function gateToolCall(\n name: string,\n args: unknown,\n deps: RunTurnDeps,\n ruleEvaluator: RuleEvaluator,\n): Promise<ReviewDecision> {\n const outcome = resolveRuleDecision(ruleEvaluator(name, args), deps.approvalPolicy);\n if (outcome === \"auto_allow\") return { kind: \"approved\" };\n if (outcome === \"auto_reject\") return DENIED;\n return deps.approvalGate.request({\n type: \"permissions.request_approval\",\n requestId: newId(\"appr\"),\n summary: `tool '${name}'`,\n });\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// src/policy.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var granularApprovalConfigSchema = z.object({
|
|
4
|
+
sandboxApproval: z.boolean(),
|
|
5
|
+
rules: z.boolean(),
|
|
6
|
+
skillApproval: z.boolean().default(false),
|
|
7
|
+
requestPermissions: z.boolean().default(false),
|
|
8
|
+
mcpElicitations: z.boolean()
|
|
9
|
+
});
|
|
10
|
+
var approvalPolicySchema = z.discriminatedUnion("kind", [
|
|
11
|
+
z.object({ kind: z.literal("unless_trusted") }),
|
|
12
|
+
z.object({ kind: z.literal("on_failure") }),
|
|
13
|
+
z.object({ kind: z.literal("on_request") }),
|
|
14
|
+
z.object({ kind: z.literal("granular"), config: granularApprovalConfigSchema }),
|
|
15
|
+
z.object({ kind: z.literal("never") })
|
|
16
|
+
]);
|
|
17
|
+
var DEFAULT_APPROVAL_POLICY = { kind: "on_request" };
|
|
18
|
+
var writableRootSchema = z.object({
|
|
19
|
+
root: z.string(),
|
|
20
|
+
readOnlySubpaths: z.array(z.string()).default([]),
|
|
21
|
+
/** Names that must never be writable even under this root (e.g. ".git/hooks"). */
|
|
22
|
+
protectedMetadataNames: z.array(z.string()).default([])
|
|
23
|
+
});
|
|
24
|
+
var capabilityPolicySchema = z.discriminatedUnion("kind", [
|
|
25
|
+
z.object({ kind: z.literal("danger_full_access") }),
|
|
26
|
+
z.object({ kind: z.literal("read_only"), networkAccess: z.boolean().default(false) }),
|
|
27
|
+
z.object({
|
|
28
|
+
kind: z.literal("external_sandbox"),
|
|
29
|
+
networkAccess: z.boolean().default(false)
|
|
30
|
+
}),
|
|
31
|
+
z.object({
|
|
32
|
+
kind: z.literal("workspace_write"),
|
|
33
|
+
writableRoots: z.array(writableRootSchema).default([]),
|
|
34
|
+
networkAccess: z.boolean().default(false),
|
|
35
|
+
excludeTmpdir: z.boolean().default(false),
|
|
36
|
+
excludeSlashTmp: z.boolean().default(false)
|
|
37
|
+
})
|
|
38
|
+
]);
|
|
39
|
+
var DEFAULT_CAPABILITY_POLICY = {
|
|
40
|
+
kind: "external_sandbox",
|
|
41
|
+
networkAccess: false
|
|
42
|
+
};
|
|
43
|
+
var DENIED = { kind: "denied" };
|
|
44
|
+
function isApproval(decision) {
|
|
45
|
+
return decision.kind === "approved" || decision.kind === "approved_rule_amendment" || decision.kind === "approved_for_session";
|
|
46
|
+
}
|
|
47
|
+
function resolveRuleDecision(rule, policy) {
|
|
48
|
+
if (rule === "forbidden") return "auto_reject";
|
|
49
|
+
if (rule === "allow") return "auto_allow";
|
|
50
|
+
switch (policy.kind) {
|
|
51
|
+
case "never":
|
|
52
|
+
return "auto_reject";
|
|
53
|
+
case "unless_trusted":
|
|
54
|
+
case "on_request":
|
|
55
|
+
case "on_failure":
|
|
56
|
+
return "ask_human";
|
|
57
|
+
case "granular":
|
|
58
|
+
return policy.config.rules ? "ask_human" : "auto_reject";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export {
|
|
63
|
+
granularApprovalConfigSchema,
|
|
64
|
+
approvalPolicySchema,
|
|
65
|
+
DEFAULT_APPROVAL_POLICY,
|
|
66
|
+
writableRootSchema,
|
|
67
|
+
capabilityPolicySchema,
|
|
68
|
+
DEFAULT_CAPABILITY_POLICY,
|
|
69
|
+
DENIED,
|
|
70
|
+
isApproval,
|
|
71
|
+
resolveRuleDecision
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=chunk-CGRCUKGT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/policy.ts"],"sourcesContent":["/**\n * Approval + capability policy (WRAP — capability #8).\n *\n * Faithful re-expression of the upstream two orthogonal axes:\n * 1. Approval policy — *when* to ask a human (AskForApproval).\n * 2. Capability policy — *what* an external executor is permitted to do\n * (SandboxPolicy). Policy/semantics only; enforcement is out of scope\n * and lives behind the external-sandbox seam (see ./sandbox).\n *\n * Plus the static rule decision (execpolicy `Decision`) and the human's rich\n * answer (`ReviewDecision`). Built on `@nebutra/permissions` for *who may\n * approve*; this module models *what tier / what answer*.\n */\n\nimport { z } from \"zod\";\n\n// ── Axis 1: approval policy ──────────────────────────────────────────────────\n\n/** Fine-grained per-category gates. `false` = auto-reject (not auto-ask). */\nexport const granularApprovalConfigSchema = z.object({\n sandboxApproval: z.boolean(),\n rules: z.boolean(),\n skillApproval: z.boolean().default(false),\n requestPermissions: z.boolean().default(false),\n mcpElicitations: z.boolean(),\n});\nexport type GranularApprovalConfig = z.infer<typeof granularApprovalConfigSchema>;\n\n/**\n * Approval tier.\n * - `unless_trusted` : only known-safe read-only ops auto-approved; else ask.\n * - `on_failure` : DEPRECATED — auto-run sandboxed, escalate on failure.\n * - `on_request` : the model decides when to ask (default).\n * - `granular` : per-category booleans.\n * - `never` : failures returned to the model, never escalated.\n */\nexport const approvalPolicySchema = z.discriminatedUnion(\"kind\", [\n z.object({ kind: z.literal(\"unless_trusted\") }),\n z.object({ kind: z.literal(\"on_failure\") }),\n z.object({ kind: z.literal(\"on_request\") }),\n z.object({ kind: z.literal(\"granular\"), config: granularApprovalConfigSchema }),\n z.object({ kind: z.literal(\"never\") }),\n]);\nexport type ApprovalPolicy = z.infer<typeof approvalPolicySchema>;\n\nexport const DEFAULT_APPROVAL_POLICY: ApprovalPolicy = { kind: \"on_request\" };\n\n// ── Axis 2: capability policy ────────────────────────────────────────────────\n\n/** A writable root with read-only sub-paths and escalation-protected names. */\nexport const writableRootSchema = z.object({\n root: z.string(),\n readOnlySubpaths: z.array(z.string()).default([]),\n /** Names that must never be writable even under this root (e.g. \".git/hooks\"). */\n protectedMetadataNames: z.array(z.string()).default([]),\n});\nexport type WritableRoot = z.infer<typeof writableRootSchema>;\n\n/**\n * Capability policy — a vocabulary of *capabilities*, not OS mechanisms.\n * Per the dual-track governance decision, `external_sandbox` is the default\n * posture: this runtime never enforces isolation itself.\n */\nexport const capabilityPolicySchema = z.discriminatedUnion(\"kind\", [\n z.object({ kind: z.literal(\"danger_full_access\") }),\n z.object({ kind: z.literal(\"read_only\"), networkAccess: z.boolean().default(false) }),\n z.object({\n kind: z.literal(\"external_sandbox\"),\n networkAccess: z.boolean().default(false),\n }),\n z.object({\n kind: z.literal(\"workspace_write\"),\n writableRoots: z.array(writableRootSchema).default([]),\n networkAccess: z.boolean().default(false),\n excludeTmpdir: z.boolean().default(false),\n excludeSlashTmp: z.boolean().default(false),\n }),\n]);\nexport type CapabilityPolicy = z.infer<typeof capabilityPolicySchema>;\n\n/** Default posture for a multi-tenant web runtime (governance: ExternalSandbox). */\nexport const DEFAULT_CAPABILITY_POLICY: CapabilityPolicy = {\n kind: \"external_sandbox\",\n networkAccess: false,\n};\n\n// ── Static rule decision (policy-as-data evaluator output) ────────────────────\n\n/** Classification of a command *before* approval. */\nexport type RuleDecision = \"allow\" | \"prompt\" | \"forbidden\";\n\n// ── The human's answer ───────────────────────────────────────────────────────\n\n/** A session-scoped amendment that remembers an allowed command prefix. */\nexport interface RuleAmendment {\n readonly allowPrefix: readonly string[];\n}\n\n/** Persist allow/deny for future requests to the same host. */\nexport interface NetworkPolicyAmendment {\n readonly host: string;\n readonly effect: \"allow\" | \"deny\";\n}\n\n/**\n * The reviewer's rich answer. Mirrors upstream `ReviewDecision`; `denied`\n * is the safe default.\n */\nexport type ReviewDecision =\n | { readonly kind: \"approved\" }\n | { readonly kind: \"approved_rule_amendment\"; readonly amendment: RuleAmendment }\n | { readonly kind: \"approved_for_session\" }\n | { readonly kind: \"network_policy_amendment\"; readonly amendment: NetworkPolicyAmendment }\n | { readonly kind: \"denied\" }\n | { readonly kind: \"timed_out\" }\n | { readonly kind: \"abort\" };\n\nexport const DENIED: ReviewDecision = { kind: \"denied\" };\n\n/** Whether a decision permits the action to proceed. */\nexport function isApproval(decision: ReviewDecision): boolean {\n return (\n decision.kind === \"approved\" ||\n decision.kind === \"approved_rule_amendment\" ||\n decision.kind === \"approved_for_session\"\n );\n}\n\n/**\n * Resolve a static rule decision against an approval policy into whether the\n * action proceeds without a human, must be asked, or is auto-rejected.\n * Pure; the actual prompting/escalation transport lives in ./protocol.\n */\nexport function resolveRuleDecision(\n rule: RuleDecision,\n policy: ApprovalPolicy,\n): \"auto_allow\" | \"ask_human\" | \"auto_reject\" {\n if (rule === \"forbidden\") return \"auto_reject\";\n if (rule === \"allow\") return \"auto_allow\";\n // rule === \"prompt\"\n switch (policy.kind) {\n case \"never\":\n return \"auto_reject\";\n case \"unless_trusted\":\n case \"on_request\":\n case \"on_failure\":\n return \"ask_human\";\n case \"granular\":\n return policy.config.rules ? \"ask_human\" : \"auto_reject\";\n }\n}\n"],"mappings":";AAcA,SAAS,SAAS;AAKX,IAAM,+BAA+B,EAAE,OAAO;AAAA,EACnD,iBAAiB,EAAE,QAAQ;AAAA,EAC3B,OAAO,EAAE,QAAQ;AAAA,EACjB,eAAe,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,EACxC,oBAAoB,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,EAC7C,iBAAiB,EAAE,QAAQ;AAC7B,CAAC;AAWM,IAAM,uBAAuB,EAAE,mBAAmB,QAAQ;AAAA,EAC/D,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,gBAAgB,EAAE,CAAC;AAAA,EAC9C,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,YAAY,EAAE,CAAC;AAAA,EAC1C,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,YAAY,EAAE,CAAC;AAAA,EAC1C,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,UAAU,GAAG,QAAQ,6BAA6B,CAAC;AAAA,EAC9E,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,OAAO,EAAE,CAAC;AACvC,CAAC;AAGM,IAAM,0BAA0C,EAAE,MAAM,aAAa;AAKrE,IAAM,qBAAqB,EAAE,OAAO;AAAA,EACzC,MAAM,EAAE,OAAO;AAAA,EACf,kBAAkB,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;AAAA;AAAA,EAEhD,wBAAwB,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;AACxD,CAAC;AAQM,IAAM,yBAAyB,EAAE,mBAAmB,QAAQ;AAAA,EACjE,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,oBAAoB,EAAE,CAAC;AAAA,EAClD,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,WAAW,GAAG,eAAe,EAAE,QAAQ,EAAE,QAAQ,KAAK,EAAE,CAAC;AAAA,EACpF,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,kBAAkB;AAAA,IAClC,eAAe,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,EAC1C,CAAC;AAAA,EACD,EAAE,OAAO;AAAA,IACP,MAAM,EAAE,QAAQ,iBAAiB;AAAA,IACjC,eAAe,EAAE,MAAM,kBAAkB,EAAE,QAAQ,CAAC,CAAC;AAAA,IACrD,eAAe,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,IACxC,eAAe,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,IACxC,iBAAiB,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,EAC5C,CAAC;AACH,CAAC;AAIM,IAAM,4BAA8C;AAAA,EACzD,MAAM;AAAA,EACN,eAAe;AACjB;AAiCO,IAAM,SAAyB,EAAE,MAAM,SAAS;AAGhD,SAAS,WAAW,UAAmC;AAC5D,SACE,SAAS,SAAS,cAClB,SAAS,SAAS,6BAClB,SAAS,SAAS;AAEtB;AAOO,SAAS,oBACd,MACA,QAC4C;AAC5C,MAAI,SAAS,YAAa,QAAO;AACjC,MAAI,SAAS,QAAS,QAAO;AAE7B,UAAQ,OAAO,MAAM;AAAA,IACnB,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AAAA,IACL,KAAK;AAAA,IACL,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO,OAAO,OAAO,QAAQ,cAAc;AAAA,EAC/C;AACF;","names":[]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// src/protocol.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function scopeKey(scope) {
|
|
4
|
+
switch (scope.kind) {
|
|
5
|
+
case "none":
|
|
6
|
+
return `t:${scope.tenantId}|none:${cryptoRandom()}`;
|
|
7
|
+
case "global":
|
|
8
|
+
case "global_shared_read":
|
|
9
|
+
return `t:${scope.tenantId}|${scope.kind}:${scope.key}`;
|
|
10
|
+
case "thread":
|
|
11
|
+
return `t:${scope.tenantId}|thread:${scope.threadId}`;
|
|
12
|
+
case "command_exec":
|
|
13
|
+
return `t:${scope.tenantId}|cmd:${scope.processId}`;
|
|
14
|
+
case "mcp_oauth":
|
|
15
|
+
return `t:${scope.tenantId}|mcpoauth:${scope.server}`;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function cryptoRandom() {
|
|
19
|
+
return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;
|
|
20
|
+
}
|
|
21
|
+
var METHOD_REGISTRY = {
|
|
22
|
+
initialize: { method: "initialize", scope: { kind: "none" } },
|
|
23
|
+
threadStart: { method: "thread/start", scope: { kind: "none" } },
|
|
24
|
+
threadResume: { method: "thread/resume", scope: { kind: "thread" } },
|
|
25
|
+
threadFork: { method: "thread/fork", scope: { kind: "thread" } },
|
|
26
|
+
threadArchive: { method: "thread/archive", scope: { kind: "thread" } },
|
|
27
|
+
threadCompactStart: { method: "thread/compact/start", scope: { kind: "thread" } },
|
|
28
|
+
threadRollback: { method: "thread/rollback", scope: { kind: "thread" } },
|
|
29
|
+
turnStart: { method: "turn/start", scope: { kind: "thread" } },
|
|
30
|
+
turnSteer: { method: "turn/steer", scope: { kind: "thread" } },
|
|
31
|
+
turnInterrupt: { method: "turn/interrupt", scope: { kind: "thread" } },
|
|
32
|
+
modelList: { method: "model/list", scope: { kind: "none" } }
|
|
33
|
+
};
|
|
34
|
+
function resolveScope(spec, tenantId, params) {
|
|
35
|
+
switch (spec.scope.kind) {
|
|
36
|
+
case "none":
|
|
37
|
+
return { tenantId, kind: "none" };
|
|
38
|
+
case "global":
|
|
39
|
+
return { tenantId, kind: "global", key: spec.scope.key };
|
|
40
|
+
case "thread": {
|
|
41
|
+
if (!params.threadId) {
|
|
42
|
+
throw new Error(`Method ${spec.method} requires a threadId for scoping`);
|
|
43
|
+
}
|
|
44
|
+
return { tenantId, kind: "thread", threadId: params.threadId };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
var NOTIFICATIONS = [
|
|
49
|
+
"thread/started",
|
|
50
|
+
"turn/started",
|
|
51
|
+
"turn/completed",
|
|
52
|
+
"turn/failed",
|
|
53
|
+
"item/started",
|
|
54
|
+
"item/updated",
|
|
55
|
+
"item/completed",
|
|
56
|
+
"model/rerouted",
|
|
57
|
+
"thread/compacted"
|
|
58
|
+
];
|
|
59
|
+
var requestEnvelopeSchema = z.object({
|
|
60
|
+
jsonrpc: z.literal("2.0"),
|
|
61
|
+
id: z.union([z.string(), z.number()]),
|
|
62
|
+
method: z.string(),
|
|
63
|
+
/** tenantId is mandatory at the envelope boundary — fail closed otherwise. */
|
|
64
|
+
tenantId: z.string().min(1),
|
|
65
|
+
params: z.unknown().optional()
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export {
|
|
69
|
+
scopeKey,
|
|
70
|
+
METHOD_REGISTRY,
|
|
71
|
+
resolveScope,
|
|
72
|
+
NOTIFICATIONS,
|
|
73
|
+
requestEnvelopeSchema
|
|
74
|
+
};
|
|
75
|
+
//# sourceMappingURL=chunk-FUG5DT2C.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/protocol.ts"],"sourcesContent":["/**\n * App-server protocol contract (WRAP — capability #5).\n *\n * Faithful re-expression of the upstream JSON-RPC method registry, the\n * serialization-scope concurrency model, server-initiated approval requests,\n * and the notification event stream.\n *\n * Multi-tenant adaptation (non-negotiable): every serialization scope is\n * additionally keyed by `tenantId`. Upstream `Thread { thread_id }` becomes\n * `{ tenantId, threadId }` — natural per-tenant ordering + isolation for free.\n * This is the only coupling seam to an optional decoupled kernel sidecar\n * (Track B); transport (WS/SSE/stdio) is pluggable and not defined here.\n */\n\nimport { z } from \"zod\";\n\n// ── Serialization scope (concurrency control) ────────────────────────────────\n\n/**\n * Requests sharing a scope are serialized; distinct scopes run in parallel.\n * `tenantId` is mandatory on every scope — cross-tenant requests can never\n * share a scope.\n */\nexport type SerializationScope =\n | { readonly tenantId: string; readonly kind: \"none\" }\n | { readonly tenantId: string; readonly kind: \"global\"; readonly key: string }\n | { readonly tenantId: string; readonly kind: \"global_shared_read\"; readonly key: string }\n | { readonly tenantId: string; readonly kind: \"thread\"; readonly threadId: string }\n | { readonly tenantId: string; readonly kind: \"command_exec\"; readonly processId: string }\n | { readonly tenantId: string; readonly kind: \"mcp_oauth\"; readonly server: string };\n\n/** Stable string key used to coalesce requests into the same serial lane. */\nexport function scopeKey(scope: SerializationScope): string {\n switch (scope.kind) {\n case \"none\":\n return `t:${scope.tenantId}|none:${cryptoRandom()}`;\n case \"global\":\n case \"global_shared_read\":\n return `t:${scope.tenantId}|${scope.kind}:${scope.key}`;\n case \"thread\":\n return `t:${scope.tenantId}|thread:${scope.threadId}`;\n case \"command_exec\":\n return `t:${scope.tenantId}|cmd:${scope.processId}`;\n case \"mcp_oauth\":\n return `t:${scope.tenantId}|mcpoauth:${scope.server}`;\n }\n}\n\nfunction cryptoRandom(): string {\n return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random()}`;\n}\n\n// ── Client -> server method registry ─────────────────────────────────────────\n\n/** How a method's scope is derived from its params. */\nexport type ScopeRule =\n | { readonly kind: \"none\" }\n | { readonly kind: \"global\"; readonly key: string }\n | { readonly kind: \"thread\" };\n\nexport interface MethodSpec {\n /** Namespaced `domain/verb` wire name. */\n readonly method: string;\n readonly scope: ScopeRule;\n readonly experimental?: boolean;\n}\n\n/**\n * The faithful subset of the upstream registry relevant to a multi-tenant web\n * product. `thread`-scoped methods derive `{ tenantId, threadId }`.\n */\nexport const METHOD_REGISTRY = {\n initialize: { method: \"initialize\", scope: { kind: \"none\" } },\n threadStart: { method: \"thread/start\", scope: { kind: \"none\" } },\n threadResume: { method: \"thread/resume\", scope: { kind: \"thread\" } },\n threadFork: { method: \"thread/fork\", scope: { kind: \"thread\" } },\n threadArchive: { method: \"thread/archive\", scope: { kind: \"thread\" } },\n threadCompactStart: { method: \"thread/compact/start\", scope: { kind: \"thread\" } },\n threadRollback: { method: \"thread/rollback\", scope: { kind: \"thread\" } },\n turnStart: { method: \"turn/start\", scope: { kind: \"thread\" } },\n turnSteer: { method: \"turn/steer\", scope: { kind: \"thread\" } },\n turnInterrupt: { method: \"turn/interrupt\", scope: { kind: \"thread\" } },\n modelList: { method: \"model/list\", scope: { kind: \"none\" } },\n} as const satisfies Record<string, MethodSpec>;\n\nexport type MethodName = (typeof METHOD_REGISTRY)[keyof typeof METHOD_REGISTRY][\"method\"];\n\n/** Resolve a method's runtime scope from tenant + params. */\nexport function resolveScope(\n spec: MethodSpec,\n tenantId: string,\n params: { threadId?: string },\n): SerializationScope {\n switch (spec.scope.kind) {\n case \"none\":\n return { tenantId, kind: \"none\" };\n case \"global\":\n return { tenantId, kind: \"global\", key: spec.scope.key };\n case \"thread\": {\n if (!params.threadId) {\n throw new Error(`Method ${spec.method} requires a threadId for scoping`);\n }\n return { tenantId, kind: \"thread\", threadId: params.threadId };\n }\n }\n}\n\n// ── Server -> client requests (approval / elicitation) ───────────────────────\n\n/** Approvals are server-initiated RPC, not side channels. */\nexport type ServerRequest =\n | {\n readonly type: \"command_execution.request_approval\";\n readonly requestId: string;\n readonly command: string;\n }\n | {\n readonly type: \"file_change.request_approval\";\n readonly requestId: string;\n readonly paths: readonly string[];\n }\n | {\n readonly type: \"permissions.request_approval\";\n readonly requestId: string;\n readonly summary: string;\n }\n | {\n readonly type: \"mcp.elicitation\";\n readonly requestId: string;\n readonly server: string;\n readonly schema: unknown;\n };\n\n// ── Server -> client notifications (event stream) ────────────────────────────\n\nexport const NOTIFICATIONS = [\n \"thread/started\",\n \"turn/started\",\n \"turn/completed\",\n \"turn/failed\",\n \"item/started\",\n \"item/updated\",\n \"item/completed\",\n \"model/rerouted\",\n \"thread/compacted\",\n] as const;\nexport type NotificationName = (typeof NOTIFICATIONS)[number];\n\n// ── Boundary validation for the universal envelope ───────────────────────────\n\nexport const requestEnvelopeSchema = z.object({\n jsonrpc: z.literal(\"2.0\"),\n id: z.union([z.string(), z.number()]),\n method: z.string(),\n /** tenantId is mandatory at the envelope boundary — fail closed otherwise. */\n tenantId: z.string().min(1),\n params: z.unknown().optional(),\n});\nexport type RequestEnvelope = z.infer<typeof requestEnvelopeSchema>;\n"],"mappings":";AAcA,SAAS,SAAS;AAkBX,SAAS,SAAS,OAAmC;AAC1D,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,KAAK,MAAM,QAAQ,SAAS,aAAa,CAAC;AAAA,IACnD,KAAK;AAAA,IACL,KAAK;AACH,aAAO,KAAK,MAAM,QAAQ,IAAI,MAAM,IAAI,IAAI,MAAM,GAAG;AAAA,IACvD,KAAK;AACH,aAAO,KAAK,MAAM,QAAQ,WAAW,MAAM,QAAQ;AAAA,IACrD,KAAK;AACH,aAAO,KAAK,MAAM,QAAQ,QAAQ,MAAM,SAAS;AAAA,IACnD,KAAK;AACH,aAAO,KAAK,MAAM,QAAQ,aAAa,MAAM,MAAM;AAAA,EACvD;AACF;AAEA,SAAS,eAAuB;AAC9B,SAAO,WAAW,QAAQ,aAAa,KAAK,GAAG,KAAK,IAAI,CAAC,IAAI,KAAK,OAAO,CAAC;AAC5E;AAqBO,IAAM,kBAAkB;AAAA,EAC7B,YAAY,EAAE,QAAQ,cAAc,OAAO,EAAE,MAAM,OAAO,EAAE;AAAA,EAC5D,aAAa,EAAE,QAAQ,gBAAgB,OAAO,EAAE,MAAM,OAAO,EAAE;AAAA,EAC/D,cAAc,EAAE,QAAQ,iBAAiB,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EACnE,YAAY,EAAE,QAAQ,eAAe,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EAC/D,eAAe,EAAE,QAAQ,kBAAkB,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EACrE,oBAAoB,EAAE,QAAQ,wBAAwB,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EAChF,gBAAgB,EAAE,QAAQ,mBAAmB,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EACvE,WAAW,EAAE,QAAQ,cAAc,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EAC7D,WAAW,EAAE,QAAQ,cAAc,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EAC7D,eAAe,EAAE,QAAQ,kBAAkB,OAAO,EAAE,MAAM,SAAS,EAAE;AAAA,EACrE,WAAW,EAAE,QAAQ,cAAc,OAAO,EAAE,MAAM,OAAO,EAAE;AAC7D;AAKO,SAAS,aACd,MACA,UACA,QACoB;AACpB,UAAQ,KAAK,MAAM,MAAM;AAAA,IACvB,KAAK;AACH,aAAO,EAAE,UAAU,MAAM,OAAO;AAAA,IAClC,KAAK;AACH,aAAO,EAAE,UAAU,MAAM,UAAU,KAAK,KAAK,MAAM,IAAI;AAAA,IACzD,KAAK,UAAU;AACb,UAAI,CAAC,OAAO,UAAU;AACpB,cAAM,IAAI,MAAM,UAAU,KAAK,MAAM,kCAAkC;AAAA,MACzE;AACA,aAAO,EAAE,UAAU,MAAM,UAAU,UAAU,OAAO,SAAS;AAAA,IAC/D;AAAA,EACF;AACF;AA8BO,IAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKO,IAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,SAAS,EAAE,QAAQ,KAAK;AAAA,EACxB,IAAI,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,EAAE,OAAO,CAAC,CAAC;AAAA,EACpC,QAAQ,EAAE,OAAO;AAAA;AAAA,EAEjB,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC;AAAA,EAC1B,QAAQ,EAAE,QAAQ,EAAE,SAAS;AAC/B,CAAC;","names":[]}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// src/hook-pipeline.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
var listeners = /* @__PURE__ */ new Set();
|
|
4
|
+
function onHookEvent(listener) {
|
|
5
|
+
listeners.add(listener);
|
|
6
|
+
return () => {
|
|
7
|
+
listeners.delete(listener);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
function emit(e) {
|
|
11
|
+
for (const l of listeners) {
|
|
12
|
+
try {
|
|
13
|
+
l(e);
|
|
14
|
+
} catch {
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function matchesMatcher(matcher, name) {
|
|
19
|
+
if (matcher === "*") return true;
|
|
20
|
+
if (matcher.length >= 2 && matcher.startsWith("/") && matcher.endsWith("/")) {
|
|
21
|
+
try {
|
|
22
|
+
return new RegExp(matcher.slice(1, -1)).test(name);
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
if (matcher.includes("|")) {
|
|
28
|
+
return matcher.split("|").some((m) => m === name);
|
|
29
|
+
}
|
|
30
|
+
return matcher === name;
|
|
31
|
+
}
|
|
32
|
+
var partialOutcomeSchema = z.object({
|
|
33
|
+
preventContinuation: z.boolean().optional(),
|
|
34
|
+
stopReason: z.string().optional(),
|
|
35
|
+
permissionBehavior: z.enum(["allow", "deny", "ask"]).optional(),
|
|
36
|
+
blockingError: z.string().optional(),
|
|
37
|
+
additionalContext: z.string().optional(),
|
|
38
|
+
updatedInput: z.unknown().optional(),
|
|
39
|
+
updatedToolOutput: z.unknown().optional(),
|
|
40
|
+
systemMessage: z.string().optional()
|
|
41
|
+
}).strict().partial();
|
|
42
|
+
function isExitResult(r) {
|
|
43
|
+
return typeof r.code === "number";
|
|
44
|
+
}
|
|
45
|
+
function exitToOutcome(r) {
|
|
46
|
+
if (r.code === 2) {
|
|
47
|
+
return {
|
|
48
|
+
preventContinuation: true,
|
|
49
|
+
blockingError: (r.stderr ?? "").trim() || "hook exited with code 2"
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
if (r.code !== 0) {
|
|
53
|
+
return {
|
|
54
|
+
preventContinuation: true,
|
|
55
|
+
blockingError: (r.stderr ?? "").trim() || `hook exited with code ${r.code}`
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return r.stdout ? { additionalContext: r.stdout } : {};
|
|
59
|
+
}
|
|
60
|
+
async function runTransport(transport, payload, ctx) {
|
|
61
|
+
if (transport.kind === "function") {
|
|
62
|
+
const result = await transport.run(payload, ctx);
|
|
63
|
+
if (isExitResult(result)) return exitToOutcome(result);
|
|
64
|
+
return partialOutcomeSchema.parse(result);
|
|
65
|
+
}
|
|
66
|
+
if (transport.kind === "prompt") {
|
|
67
|
+
const result = await transport.llmEval(payload, ctx);
|
|
68
|
+
return partialOutcomeSchema.parse(result);
|
|
69
|
+
}
|
|
70
|
+
let url;
|
|
71
|
+
try {
|
|
72
|
+
url = new URL(transport.url);
|
|
73
|
+
} catch {
|
|
74
|
+
return failClosed(`invalid hook url: ${transport.url}`);
|
|
75
|
+
}
|
|
76
|
+
if (url.protocol !== "https:") {
|
|
77
|
+
return failClosed(`hook url must be https (got ${url.protocol})`);
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const res = await transport.fetchImpl(url.toString(), {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "content-type": "application/json", "x-tenant-id": ctx.tenantId },
|
|
83
|
+
body: JSON.stringify({ payload, tenantId: ctx.tenantId, threadId: ctx.threadId })
|
|
84
|
+
});
|
|
85
|
+
if (res.status < 200 || res.status >= 300) {
|
|
86
|
+
return failClosed(`hook endpoint returned ${res.status}`);
|
|
87
|
+
}
|
|
88
|
+
const json = await res.json();
|
|
89
|
+
return partialOutcomeSchema.parse(json);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return failClosed(`hook transport failed: ${String(err)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function failClosed(reason) {
|
|
95
|
+
return { preventContinuation: true, blockingError: reason };
|
|
96
|
+
}
|
|
97
|
+
var DENY_PRECEDENCE = ["deny", "ask", "allow"];
|
|
98
|
+
function mergeOutcomes(parts) {
|
|
99
|
+
const contexts = [];
|
|
100
|
+
let permission;
|
|
101
|
+
let prevent = false;
|
|
102
|
+
let blockingError;
|
|
103
|
+
let stopReason;
|
|
104
|
+
let systemMessage;
|
|
105
|
+
let updatedInput;
|
|
106
|
+
let updatedToolOutput;
|
|
107
|
+
for (const p of parts) {
|
|
108
|
+
if (p.additionalContext !== void 0) contexts.push(p.additionalContext);
|
|
109
|
+
if (p.preventContinuation) prevent = true;
|
|
110
|
+
if (p.blockingError !== void 0 && blockingError === void 0) {
|
|
111
|
+
blockingError = p.blockingError;
|
|
112
|
+
}
|
|
113
|
+
if (p.stopReason !== void 0 && stopReason === void 0) stopReason = p.stopReason;
|
|
114
|
+
if (p.systemMessage !== void 0 && systemMessage === void 0) {
|
|
115
|
+
systemMessage = p.systemMessage;
|
|
116
|
+
}
|
|
117
|
+
if (p.permissionBehavior !== void 0) {
|
|
118
|
+
if (permission === void 0 || DENY_PRECEDENCE.indexOf(p.permissionBehavior) < DENY_PRECEDENCE.indexOf(permission)) {
|
|
119
|
+
permission = p.permissionBehavior;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if ("updatedInput" in p) updatedInput = { value: p.updatedInput };
|
|
123
|
+
if ("updatedToolOutput" in p) updatedToolOutput = { value: p.updatedToolOutput };
|
|
124
|
+
}
|
|
125
|
+
if (blockingError !== void 0) prevent = true;
|
|
126
|
+
if (permission === "deny") prevent = prevent || false;
|
|
127
|
+
const outcome = {};
|
|
128
|
+
if (prevent) outcome.preventContinuation = true;
|
|
129
|
+
if (blockingError !== void 0) outcome.blockingError = blockingError;
|
|
130
|
+
if (stopReason !== void 0) outcome.stopReason = stopReason;
|
|
131
|
+
if (systemMessage !== void 0) outcome.systemMessage = systemMessage;
|
|
132
|
+
if (permission !== void 0) outcome.permissionBehavior = permission;
|
|
133
|
+
if (contexts.length > 0) outcome.additionalContext = contexts.join("\n");
|
|
134
|
+
if (updatedInput !== void 0) outcome.updatedInput = updatedInput.value;
|
|
135
|
+
if (updatedToolOutput !== void 0) outcome.updatedToolOutput = updatedToolOutput.value;
|
|
136
|
+
return outcome;
|
|
137
|
+
}
|
|
138
|
+
async function runHooks(event, payload, hooks, ctx) {
|
|
139
|
+
if (!ctx || typeof ctx.tenantId !== "string" || ctx.tenantId.trim() === "") {
|
|
140
|
+
throw new Error("runHooks: tenantId is mandatory and must be non-empty (fail-closed)");
|
|
141
|
+
}
|
|
142
|
+
const selected = hooks.filter(
|
|
143
|
+
(h) => h.event === event && matchesMatcher(h.matcher, payload.name) && (h.ifCondition === void 0 || h.ifCondition(payload.input, ctx) === true)
|
|
144
|
+
);
|
|
145
|
+
if (selected.length === 0) return {};
|
|
146
|
+
const results = await Promise.all(
|
|
147
|
+
selected.map(async (h) => {
|
|
148
|
+
emit({ phase: "started", event, matcher: h.matcher, tenantId: ctx.tenantId });
|
|
149
|
+
try {
|
|
150
|
+
return await runTransport(h.transport, payload, ctx);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
return failClosed(`hook crashed: ${String(err)}`);
|
|
153
|
+
} finally {
|
|
154
|
+
emit({ phase: "completed", event, matcher: h.matcher, tenantId: ctx.tenantId });
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
);
|
|
158
|
+
return mergeOutcomes(results);
|
|
159
|
+
}
|
|
160
|
+
function fromToolHooks(toolHooks) {
|
|
161
|
+
const configs = [];
|
|
162
|
+
if (toolHooks.preToolUse) {
|
|
163
|
+
const pre = toolHooks.preToolUse;
|
|
164
|
+
configs.push({
|
|
165
|
+
event: "PreToolUse",
|
|
166
|
+
matcher: "*",
|
|
167
|
+
transport: {
|
|
168
|
+
kind: "function",
|
|
169
|
+
run: async (payload, ctx) => {
|
|
170
|
+
await pre(payload.name, payload.input, ctx);
|
|
171
|
+
return {};
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (toolHooks.postToolUse) {
|
|
177
|
+
const post = toolHooks.postToolUse;
|
|
178
|
+
configs.push({
|
|
179
|
+
event: "PostToolUse",
|
|
180
|
+
matcher: "*",
|
|
181
|
+
transport: {
|
|
182
|
+
kind: "function",
|
|
183
|
+
run: async (payload, ctx) => {
|
|
184
|
+
await post(payload.name, payload.toolOutput, ctx);
|
|
185
|
+
return {};
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return configs;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export {
|
|
194
|
+
onHookEvent,
|
|
195
|
+
matchesMatcher,
|
|
196
|
+
runHooks,
|
|
197
|
+
fromToolHooks
|
|
198
|
+
};
|
|
199
|
+
//# sourceMappingURL=chunk-LO24VOA3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/hook-pipeline.ts"],"sourcesContent":["/**\n * Hook pipeline (WRAP — config-driven orchestrator hooks).\n *\n * Faithful re-expression of the upstream config-driven, decision-returning\n * hook system. This is the configuration layer ABOVE the bare {@link ToolHooks}\n * interface in `./tools.js`: events are matched by an event + matcher\n * (+ optional ifCondition), fanned out to transports in parallel, and merged\n * into a single deterministic {@link HookOutcome}.\n *\n * MULTI-TENANT SAFETY — the upstream `command`/shell/async (host-process)\n * transports are deliberately DROPPED. In a multi-tenant runtime, executing a\n * tenant-supplied host command is unbounded RCE against the shared process and\n * cannot be sandboxed at this layer. Only in-process `function`, caller-fetched\n * `http`, and caller-injected `prompt` transports are supported — each fails\n * closed and cannot reach the host shell.\n */\n\nimport { z } from \"zod\";\nimport type { ToolDispatchContext, ToolHooks } from \"./tools\";\n\n/**\n * Event taxonomy — ported verbatim from the upstream hook system.\n * The matcher is matched against the tool name (tool events) or the event\n * name itself (lifecycle events).\n */\nexport type HookEvent =\n | \"PreToolUse\"\n | \"PostToolUse\"\n | \"PostToolUseFailure\"\n | \"UserPromptSubmit\"\n | \"SessionStart\"\n | \"SessionEnd\"\n | \"Stop\"\n | \"StopFailure\"\n | \"SubagentStart\"\n | \"SubagentStop\"\n | \"PreCompact\"\n | \"PostCompact\"\n | \"PermissionRequest\"\n | \"PermissionDenied\"\n | \"TaskCreated\"\n | \"TaskCompleted\"\n | \"ConfigChange\"\n | \"FileChanged\";\n\n/** Tenant-scoped context — reuses the dispatch context shape from `./tools.js`. */\nexport type HookContext = ToolDispatchContext;\n\n/** The event payload presented to a transport. */\nexport interface HookPayload {\n /** The tool name (tool events) or event name (lifecycle events). */\n readonly name: string;\n /** Parsed tool/event input. */\n readonly input: unknown;\n /** Tool output (only present for Post* events). */\n readonly toolOutput?: unknown;\n}\n\n/**\n * Decision protocol — ported verbatim. A transport returns a PARTIAL outcome;\n * the pipeline merges all partials with deterministic precedence.\n */\nexport interface HookOutcome {\n readonly preventContinuation?: boolean | undefined;\n readonly stopReason?: string | undefined;\n readonly permissionBehavior?: \"allow\" | \"deny\" | \"ask\" | undefined;\n readonly blockingError?: string | undefined;\n readonly additionalContext?: string | undefined;\n readonly updatedInput?: unknown;\n readonly updatedToolOutput?: unknown;\n readonly systemMessage?: string | undefined;\n}\n\n/** Exit-code-style lane for the function transport. */\nexport interface HookExitResult {\n readonly code: number;\n readonly stdout?: string;\n readonly stderr?: string;\n}\n\ntype FunctionRunResult = Partial<HookOutcome> | HookExitResult;\n\n/** In-process transport — the tenant-safe default. */\nexport interface FunctionTransport {\n readonly kind: \"function\";\n readonly run: (payload: HookPayload, ctx: HookContext) => Promise<FunctionRunResult>;\n}\n\n/** HTTP transport — caller injects `fetchImpl`; absolute https + fail-closed. */\nexport interface HttpTransport {\n readonly kind: \"http\";\n readonly url: string;\n readonly allowEnvVars?: readonly string[];\n readonly fetchImpl: (url: string, init: RequestInit) => Promise<Response>;\n}\n\n/** Prompt transport — caller injects the LLM evaluator. */\nexport interface PromptTransport {\n readonly kind: \"prompt\";\n readonly model: string;\n readonly llmEval: (payload: HookPayload, ctx: HookContext) => Promise<Partial<HookOutcome>>;\n}\n\nexport type HookTransport = FunctionTransport | HttpTransport | PromptTransport;\n\n/** A single hook configuration entry. */\nexport interface HookConfig {\n readonly event: HookEvent;\n /** `\"*\"` | exact | pipe-list (`\"Write|Edit\"`) | `/regex/`. */\n readonly matcher: string;\n /** Predicate over parsed input, evaluated BEFORE the transport runs. */\n readonly ifCondition?: (input: unknown, ctx: HookContext) => boolean;\n readonly transport: HookTransport;\n}\n\n/** Out-of-band progress event — decoupled from outcomes. */\nexport interface HookProgressEvent {\n readonly phase: \"started\" | \"completed\";\n readonly event: HookEvent;\n readonly matcher: string;\n readonly tenantId: string;\n}\n\ntype HookProgressListener = (e: HookProgressEvent) => void;\n\nconst listeners = new Set<HookProgressListener>();\n\n/** Subscribe to out-of-band hook progress. Returns an unsubscribe function. */\nexport function onHookEvent(listener: HookProgressListener): () => void {\n listeners.add(listener);\n return () => {\n listeners.delete(listener);\n };\n}\n\nfunction emit(e: HookProgressEvent): void {\n for (const l of listeners) {\n try {\n l(e);\n } catch {\n // progress listeners must never affect pipeline outcomes\n }\n }\n}\n\n/**\n * Pure matcher: `\"*\"` | exact | pipe-list | `/regex/`.\n */\nexport function matchesMatcher(matcher: string, name: string): boolean {\n if (matcher === \"*\") return true;\n if (matcher.length >= 2 && matcher.startsWith(\"/\") && matcher.endsWith(\"/\")) {\n try {\n return new RegExp(matcher.slice(1, -1)).test(name);\n } catch {\n return false;\n }\n }\n if (matcher.includes(\"|\")) {\n return matcher.split(\"|\").some((m) => m === name);\n }\n return matcher === name;\n}\n\nconst partialOutcomeSchema: z.ZodType<Partial<HookOutcome>> = z\n .object({\n preventContinuation: z.boolean().optional(),\n stopReason: z.string().optional(),\n permissionBehavior: z.enum([\"allow\", \"deny\", \"ask\"]).optional(),\n blockingError: z.string().optional(),\n additionalContext: z.string().optional(),\n updatedInput: z.unknown().optional(),\n updatedToolOutput: z.unknown().optional(),\n systemMessage: z.string().optional(),\n })\n .strict()\n .partial();\n\nfunction isExitResult(r: FunctionRunResult): r is HookExitResult {\n return typeof (r as HookExitResult).code === \"number\";\n}\n\nfunction exitToOutcome(r: HookExitResult): Partial<HookOutcome> {\n if (r.code === 2) {\n return {\n preventContinuation: true,\n blockingError: (r.stderr ?? \"\").trim() || \"hook exited with code 2\",\n };\n }\n if (r.code !== 0) {\n return {\n preventContinuation: true,\n blockingError: (r.stderr ?? \"\").trim() || `hook exited with code ${r.code}`,\n };\n }\n return r.stdout ? { additionalContext: r.stdout } : {};\n}\n\nasync function runTransport(\n transport: HookTransport,\n payload: HookPayload,\n ctx: HookContext,\n): Promise<Partial<HookOutcome>> {\n if (transport.kind === \"function\") {\n const result = await transport.run(payload, ctx);\n if (isExitResult(result)) return exitToOutcome(result);\n return partialOutcomeSchema.parse(result);\n }\n\n if (transport.kind === \"prompt\") {\n const result = await transport.llmEval(payload, ctx);\n return partialOutcomeSchema.parse(result);\n }\n\n // http — fail-closed on non-https / non-2xx / unparseable body.\n let url: URL;\n try {\n url = new URL(transport.url);\n } catch {\n return failClosed(`invalid hook url: ${transport.url}`);\n }\n if (url.protocol !== \"https:\") {\n return failClosed(`hook url must be https (got ${url.protocol})`);\n }\n try {\n const res = await transport.fetchImpl(url.toString(), {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", \"x-tenant-id\": ctx.tenantId },\n body: JSON.stringify({ payload, tenantId: ctx.tenantId, threadId: ctx.threadId }),\n });\n if (res.status < 200 || res.status >= 300) {\n return failClosed(`hook endpoint returned ${res.status}`);\n }\n const json: unknown = await res.json();\n return partialOutcomeSchema.parse(json);\n } catch (err) {\n return failClosed(`hook transport failed: ${String(err)}`);\n }\n}\n\nfunction failClosed(reason: string): Partial<HookOutcome> {\n return { preventContinuation: true, blockingError: reason };\n}\n\nconst DENY_PRECEDENCE: ReadonlyArray<HookOutcome[\"permissionBehavior\"]> = [\"deny\", \"ask\", \"allow\"];\n\nfunction mergeOutcomes(parts: ReadonlyArray<Partial<HookOutcome>>): HookOutcome {\n const contexts: string[] = [];\n let permission: HookOutcome[\"permissionBehavior\"];\n let prevent = false;\n let blockingError: string | undefined;\n let stopReason: string | undefined;\n let systemMessage: string | undefined;\n let updatedInput: { value: unknown } | undefined;\n let updatedToolOutput: { value: unknown } | undefined;\n\n for (const p of parts) {\n if (p.additionalContext !== undefined) contexts.push(p.additionalContext);\n if (p.preventContinuation) prevent = true;\n if (p.blockingError !== undefined && blockingError === undefined) {\n blockingError = p.blockingError;\n }\n if (p.stopReason !== undefined && stopReason === undefined) stopReason = p.stopReason;\n if (p.systemMessage !== undefined && systemMessage === undefined) {\n systemMessage = p.systemMessage;\n }\n if (p.permissionBehavior !== undefined) {\n if (\n permission === undefined ||\n DENY_PRECEDENCE.indexOf(p.permissionBehavior) < DENY_PRECEDENCE.indexOf(permission)\n ) {\n permission = p.permissionBehavior;\n }\n }\n // last writer wins, in stable (input) order\n if (\"updatedInput\" in p) updatedInput = { value: p.updatedInput };\n if (\"updatedToolOutput\" in p) updatedToolOutput = { value: p.updatedToolOutput };\n }\n\n if (blockingError !== undefined) prevent = true;\n if (permission === \"deny\") prevent = prevent || false;\n\n const outcome: Mutable<HookOutcome> = {};\n if (prevent) outcome.preventContinuation = true;\n if (blockingError !== undefined) outcome.blockingError = blockingError;\n if (stopReason !== undefined) outcome.stopReason = stopReason;\n if (systemMessage !== undefined) outcome.systemMessage = systemMessage;\n if (permission !== undefined) outcome.permissionBehavior = permission;\n if (contexts.length > 0) outcome.additionalContext = contexts.join(\"\\n\");\n if (updatedInput !== undefined) outcome.updatedInput = updatedInput.value;\n if (updatedToolOutput !== undefined) outcome.updatedToolOutput = updatedToolOutput.value;\n return outcome;\n}\n\ntype Mutable<T> = { -readonly [K in keyof T]: T[K] };\n\n/**\n * Select matching hooks, fan out in parallel, merge with deterministic\n * precedence. tenantId is mandatory — empty fails closed (throws).\n */\nexport async function runHooks(\n event: HookEvent,\n payload: HookPayload,\n hooks: ReadonlyArray<HookConfig>,\n ctx: HookContext,\n): Promise<HookOutcome> {\n if (!ctx || typeof ctx.tenantId !== \"string\" || ctx.tenantId.trim() === \"\") {\n throw new Error(\"runHooks: tenantId is mandatory and must be non-empty (fail-closed)\");\n }\n\n const selected = hooks.filter(\n (h) =>\n h.event === event &&\n matchesMatcher(h.matcher, payload.name) &&\n (h.ifCondition === undefined || h.ifCondition(payload.input, ctx) === true),\n );\n\n if (selected.length === 0) return {};\n\n // Stable order = config order, preserved despite parallel execution.\n const results = await Promise.all(\n selected.map(async (h) => {\n emit({ phase: \"started\", event, matcher: h.matcher, tenantId: ctx.tenantId });\n try {\n return await runTransport(h.transport, payload, ctx);\n } catch (err) {\n return failClosed(`hook crashed: ${String(err)}`);\n } finally {\n emit({ phase: \"completed\", event, matcher: h.matcher, tenantId: ctx.tenantId });\n }\n }),\n );\n\n return mergeOutcomes(results);\n}\n\n/**\n * Adapt the bare {@link ToolHooks} interface (from `./tools.js`) into\n * config-driven function-transport entries — the in-process transport feeding\n * the lower layer. preToolUse/postToolUse are void-returning, so the produced\n * outcome is always empty (they observe; they do not decide here).\n */\nexport function fromToolHooks(toolHooks: ToolHooks): HookConfig[] {\n const configs: HookConfig[] = [];\n if (toolHooks.preToolUse) {\n const pre = toolHooks.preToolUse;\n configs.push({\n event: \"PreToolUse\",\n matcher: \"*\",\n transport: {\n kind: \"function\",\n run: async (payload, ctx) => {\n await pre(payload.name, payload.input, ctx);\n return {};\n },\n },\n });\n }\n if (toolHooks.postToolUse) {\n const post = toolHooks.postToolUse;\n configs.push({\n event: \"PostToolUse\",\n matcher: \"*\",\n transport: {\n kind: \"function\",\n run: async (payload, ctx) => {\n await post(payload.name, payload.toolOutput, ctx);\n return {};\n },\n },\n });\n }\n return configs;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// src/sandbox.ts
|
|
2
|
+
var NoExecutorConfiguredError = class extends Error {
|
|
3
|
+
constructor() {
|
|
4
|
+
super(
|
|
5
|
+
"No ExternalSandbox configured. This runtime never executes untrusted code in-process; delegate to a decoupled isolator (Track B)."
|
|
6
|
+
);
|
|
7
|
+
this.name = "NoExecutorConfiguredError";
|
|
8
|
+
}
|
|
9
|
+
};
|
|
10
|
+
var REFUSING_SANDBOX = {
|
|
11
|
+
async exec() {
|
|
12
|
+
throw new NoExecutorConfiguredError();
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
var SandboxDelegationError = class extends Error {
|
|
16
|
+
constructor(message, status) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.status = status;
|
|
19
|
+
this.name = "SandboxDelegationError";
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
function createHttpSandbox(baseUrl, fetchImpl = fetch) {
|
|
23
|
+
const endpoint = `${baseUrl.replace(/\/$/, "")}/api/v1/sandbox/exec`;
|
|
24
|
+
return {
|
|
25
|
+
async exec(request) {
|
|
26
|
+
const response = await fetchImpl(endpoint, {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "content-type": "application/json" },
|
|
29
|
+
body: JSON.stringify(request)
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok) {
|
|
32
|
+
const detail = await response.text().catch(() => "");
|
|
33
|
+
throw new SandboxDelegationError(
|
|
34
|
+
`Isolator refused execution (${response.status}): ${detail}`,
|
|
35
|
+
response.status
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return await response.json();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function assertSafePosture(policy, allowDanger = false) {
|
|
43
|
+
if (policy.kind === "danger_full_access" && !allowDanger) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
"danger_full_access capability policy refused: not permitted for multi-tenant delegation without explicit opt-in."
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export {
|
|
51
|
+
NoExecutorConfiguredError,
|
|
52
|
+
REFUSING_SANDBOX,
|
|
53
|
+
SandboxDelegationError,
|
|
54
|
+
createHttpSandbox,
|
|
55
|
+
assertSafePosture
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=chunk-MUF7ZZTO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/sandbox.ts"],"sourcesContent":["/**\n * External-sandbox delegation seam (PORT — capability #11).\n *\n * Governance decision (dual-track, ExternalSandbox posture): this multi-tenant\n * web runtime NEVER executes untrusted code itself and introduces no isolation\n * infrastructure. It only:\n * 1. carries the capability policy (see ./policy),\n * 2. delegates execution to an external isolator behind this interface\n * (a self-hosted kernel sidecar — Track B — or any other executor),\n * 3. records the outcome as a `command_execution` item (see ./model).\n *\n * The upstream OS enforcers (Seatbelt / Landlock / bubblewrap / Windows\n * restricted token) are deliberately NOT ported — they are single-host and\n * out of scope for a multi-tenant web product.\n */\n\nimport type { CapabilityPolicy } from \"./policy\";\n\nexport interface SandboxExecRequest {\n /** Mandatory tenant scope — every delegated exec is tenant-bound. */\n readonly tenantId: string;\n readonly threadId: string;\n readonly command: string;\n /** The capabilities the external isolator must honor. */\n readonly capabilityPolicy: CapabilityPolicy;\n /** Optional inputs the executor should materialize before running. */\n readonly inputs?: Readonly<Record<string, string>>;\n}\n\nexport interface SandboxExecResult {\n readonly exitCode: number;\n readonly aggregatedOutput: string;\n /** Identifier of the isolator that actually ran the command. */\n readonly executedOn: string;\n}\n\n/**\n * The only thing Track A depends on for execution. Implemented by a decoupled\n * isolator over the ./protocol contract; never implemented in-process here.\n */\nexport interface ExternalSandbox {\n exec(request: SandboxExecRequest): Promise<SandboxExecResult>;\n}\n\n/**\n * Fail-closed default. Wiring this in production requires a real\n * {@link ExternalSandbox}; absent one, untrusted code never runs — by design.\n */\nexport class NoExecutorConfiguredError extends Error {\n constructor() {\n super(\n \"No ExternalSandbox configured. This runtime never executes untrusted \" +\n \"code in-process; delegate to a decoupled isolator (Track B).\",\n );\n this.name = \"NoExecutorConfiguredError\";\n }\n}\n\nexport const REFUSING_SANDBOX: ExternalSandbox = {\n async exec(): Promise<SandboxExecResult> {\n throw new NoExecutorConfiguredError();\n },\n};\n\nexport class SandboxDelegationError extends Error {\n constructor(\n message: string,\n readonly status: number,\n ) {\n super(message);\n this.name = \"SandboxDelegationError\";\n }\n}\n\n/**\n * The concrete Track-B coupling: delegate execution over HTTP to the decoupled\n * Rust isolator (`backends/rust/sandbox`, `POST /api/v1/sandbox/exec`). The\n * isolator is fail-closed; a non-2xx (e.g. 403 refusal) is surfaced as an\n * error and never coerced into a fabricated result.\n */\nexport function createHttpSandbox(\n baseUrl: string,\n fetchImpl: typeof fetch = fetch,\n): ExternalSandbox {\n const endpoint = `${baseUrl.replace(/\\/$/, \"\")}/api/v1/sandbox/exec`;\n return {\n async exec(request: SandboxExecRequest): Promise<SandboxExecResult> {\n const response = await fetchImpl(endpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify(request),\n });\n if (!response.ok) {\n const detail = await response.text().catch(() => \"\");\n throw new SandboxDelegationError(\n `Isolator refused execution (${response.status}): ${detail}`,\n response.status,\n );\n }\n return (await response.json()) as SandboxExecResult;\n },\n };\n}\n\n/** Guard: reject the most dangerous posture unless explicitly opted in. */\nexport function assertSafePosture(policy: CapabilityPolicy, allowDanger = false): void {\n if (policy.kind === \"danger_full_access\" && !allowDanger) {\n throw new Error(\n \"danger_full_access capability policy refused: not permitted for \" +\n \"multi-tenant delegation without explicit opt-in.\",\n );\n }\n}\n"],"mappings":";AAgDO,IAAM,4BAAN,cAAwC,MAAM;AAAA,EACnD,cAAc;AACZ;AAAA,MACE;AAAA,IAEF;AACA,SAAK,OAAO;AAAA,EACd;AACF;AAEO,IAAM,mBAAoC;AAAA,EAC/C,MAAM,OAAmC;AACvC,UAAM,IAAI,0BAA0B;AAAA,EACtC;AACF;AAEO,IAAM,yBAAN,cAAqC,MAAM;AAAA,EAChD,YACE,SACS,QACT;AACA,UAAM,OAAO;AAFJ;AAGT,SAAK,OAAO;AAAA,EACd;AACF;AAQO,SAAS,kBACd,SACA,YAA0B,OACT;AACjB,QAAM,WAAW,GAAG,QAAQ,QAAQ,OAAO,EAAE,CAAC;AAC9C,SAAO;AAAA,IACL,MAAM,KAAK,SAAyD;AAClE,YAAM,WAAW,MAAM,UAAU,UAAU;AAAA,QACzC,QAAQ;AAAA,QACR,SAAS,EAAE,gBAAgB,mBAAmB;AAAA,QAC9C,MAAM,KAAK,UAAU,OAAO;AAAA,MAC9B,CAAC;AACD,UAAI,CAAC,SAAS,IAAI;AAChB,cAAM,SAAS,MAAM,SAAS,KAAK,EAAE,MAAM,MAAM,EAAE;AACnD,cAAM,IAAI;AAAA,UACR,+BAA+B,SAAS,MAAM,MAAM,MAAM;AAAA,UAC1D,SAAS;AAAA,QACX;AAAA,MACF;AACA,aAAQ,MAAM,SAAS,KAAK;AAAA,IAC9B;AAAA,EACF;AACF;AAGO,SAAS,kBAAkB,QAA0B,cAAc,OAAa;AACrF,MAAI,OAAO,SAAS,wBAAwB,CAAC,aAAa;AACxD,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACF;","names":[]}
|