@wrenai/genbi 0.0.1
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 +201 -0
- package/README.md +141 -0
- package/bin/genbi.mjs +31 -0
- package/dist/assets/echarts-LTFC-ysd.js +40 -0
- package/dist/assets/index-Bo--QMUN.js +441 -0
- package/dist/assets/index-Xs4q7pNo.css +1 -0
- package/dist/index.html +14 -0
- package/dist/wrenai_logo.png +0 -0
- package/dist/wrenai_logo_white.png +0 -0
- package/dist-server/harness/auth/detect.d.ts +18 -0
- package/dist-server/harness/auth/detect.js +37 -0
- package/dist-server/harness/auth/index.d.ts +4 -0
- package/dist-server/harness/auth/index.js +2 -0
- package/dist-server/harness/auth/probe.d.ts +18 -0
- package/dist-server/harness/auth/probe.js +52 -0
- package/dist-server/harness/auth/types.d.ts +42 -0
- package/dist-server/harness/auth/types.js +9 -0
- package/dist-server/harness/bundle/loader.d.ts +32 -0
- package/dist-server/harness/bundle/loader.js +43 -0
- package/dist-server/harness/bundle/schema.d.ts +199 -0
- package/dist-server/harness/bundle/schema.js +127 -0
- package/dist-server/harness/bundle/version.d.ts +10 -0
- package/dist-server/harness/bundle/version.js +43 -0
- package/dist-server/harness/capability/gate.d.ts +6 -0
- package/dist-server/harness/capability/gate.js +29 -0
- package/dist-server/harness/capability/registry.d.ts +6 -0
- package/dist-server/harness/capability/registry.js +17 -0
- package/dist-server/harness/cli-args.d.ts +123 -0
- package/dist-server/harness/cli-args.js +247 -0
- package/dist-server/harness/cli.d.ts +2 -0
- package/dist-server/harness/cli.js +151 -0
- package/dist-server/harness/compile/cache.d.ts +23 -0
- package/dist-server/harness/compile/cache.js +109 -0
- package/dist-server/harness/compile/compose-profile.d.ts +34 -0
- package/dist-server/harness/compile/compose-profile.js +95 -0
- package/dist-server/harness/compile/errors.d.ts +22 -0
- package/dist-server/harness/compile/errors.js +41 -0
- package/dist-server/harness/compile/fingerprint.d.ts +22 -0
- package/dist-server/harness/compile/fingerprint.js +84 -0
- package/dist-server/harness/compile/index.d.ts +8 -0
- package/dist-server/harness/compile/index.js +7 -0
- package/dist-server/harness/compile/pipeline.d.ts +66 -0
- package/dist-server/harness/compile/pipeline.js +176 -0
- package/dist-server/harness/compile/resolve-binary.d.ts +55 -0
- package/dist-server/harness/compile/resolve-binary.js +133 -0
- package/dist-server/harness/compile/types.d.ts +107 -0
- package/dist-server/harness/compile/types.js +1 -0
- package/dist-server/harness/compile/warble-identity.d.ts +12 -0
- package/dist-server/harness/compile/warble-identity.js +33 -0
- package/dist-server/harness/compliance/gate.d.ts +35 -0
- package/dist-server/harness/compliance/gate.js +48 -0
- package/dist-server/harness/compliance/index.d.ts +2 -0
- package/dist-server/harness/compliance/index.js +1 -0
- package/dist-server/harness/compliance/types.d.ts +16 -0
- package/dist-server/harness/compliance/types.js +1 -0
- package/dist-server/harness/events/emitter.d.ts +34 -0
- package/dist-server/harness/events/emitter.js +25 -0
- package/dist-server/harness/events/index.d.ts +4 -0
- package/dist-server/harness/events/index.js +2 -0
- package/dist-server/harness/events/sse.d.ts +20 -0
- package/dist-server/harness/events/sse.js +48 -0
- package/dist-server/harness/events/types.d.ts +131 -0
- package/dist-server/harness/events/types.js +1 -0
- package/dist-server/harness/exec/errors.d.ts +16 -0
- package/dist-server/harness/exec/errors.js +29 -0
- package/dist-server/harness/exec/index.d.ts +5 -0
- package/dist-server/harness/exec/index.js +2 -0
- package/dist-server/harness/exec/local.d.ts +32 -0
- package/dist-server/harness/exec/local.js +134 -0
- package/dist-server/harness/exec/policy.d.ts +12 -0
- package/dist-server/harness/exec/policy.js +1 -0
- package/dist-server/harness/exec/types.d.ts +63 -0
- package/dist-server/harness/exec/types.js +1 -0
- package/dist-server/harness/guardrails/index.d.ts +2 -0
- package/dist-server/harness/guardrails/index.js +1 -0
- package/dist-server/harness/guardrails/policy.d.ts +45 -0
- package/dist-server/harness/guardrails/policy.js +57 -0
- package/dist-server/harness/index.d.ts +37 -0
- package/dist-server/harness/index.js +19 -0
- package/dist-server/harness/loop/errors.d.ts +53 -0
- package/dist-server/harness/loop/errors.js +72 -0
- package/dist-server/harness/loop/executor.d.ts +123 -0
- package/dist-server/harness/loop/executor.js +380 -0
- package/dist-server/harness/loop/guard.d.ts +16 -0
- package/dist-server/harness/loop/guard.js +1 -0
- package/dist-server/harness/loop/index.d.ts +5 -0
- package/dist-server/harness/loop/index.js +3 -0
- package/dist-server/harness/native-producer/index.d.ts +109 -0
- package/dist-server/harness/native-producer/index.js +302 -0
- package/dist-server/harness/npm-package-resolve.d.ts +28 -0
- package/dist-server/harness/npm-package-resolve.js +47 -0
- package/dist-server/harness/providers/adapters/anthropic.d.ts +7 -0
- package/dist-server/harness/providers/adapters/anthropic.js +6 -0
- package/dist-server/harness/providers/adapters/mock.d.ts +4 -0
- package/dist-server/harness/providers/adapters/mock.js +5 -0
- package/dist-server/harness/providers/adapters/openai-compatible.d.ts +10 -0
- package/dist-server/harness/providers/adapters/openai-compatible.js +10 -0
- package/dist-server/harness/providers/binding.d.ts +23 -0
- package/dist-server/harness/providers/binding.js +13 -0
- package/dist-server/harness/providers/errors.d.ts +6 -0
- package/dist-server/harness/providers/errors.js +12 -0
- package/dist-server/harness/providers/index.d.ts +18 -0
- package/dist-server/harness/providers/index.js +22 -0
- package/dist-server/harness/providers/registry.d.ts +16 -0
- package/dist-server/harness/providers/registry.js +24 -0
- package/dist-server/harness/render/envelope.d.ts +145 -0
- package/dist-server/harness/render/envelope.js +428 -0
- package/dist-server/harness/render/errors.d.ts +33 -0
- package/dist-server/harness/render/errors.js +46 -0
- package/dist-server/harness/render/index.d.ts +5 -0
- package/dist-server/harness/render/index.js +3 -0
- package/dist-server/harness/render/validate.d.ts +13 -0
- package/dist-server/harness/render/validate.js +88 -0
- package/dist-server/harness/route/adapter-spec.d.ts +40 -0
- package/dist-server/harness/route/adapter-spec.js +61 -0
- package/dist-server/harness/route/agent-sdk-cli.d.ts +30 -0
- package/dist-server/harness/route/agent-sdk-cli.js +86 -0
- package/dist-server/harness/route/agent-sdk-manifest.d.ts +31 -0
- package/dist-server/harness/route/agent-sdk-manifest.js +56 -0
- package/dist-server/harness/route/artifact-content.d.ts +53 -0
- package/dist-server/harness/route/artifact-content.js +83 -0
- package/dist-server/harness/route/chat-event-mapper.d.ts +95 -0
- package/dist-server/harness/route/chat-event-mapper.js +164 -0
- package/dist-server/harness/route/codex-ask-events.d.ts +25 -0
- package/dist-server/harness/route/codex-ask-events.js +395 -0
- package/dist-server/harness/route/codex-ask.d.ts +15 -0
- package/dist-server/harness/route/codex-ask.js +242 -0
- package/dist-server/harness/route/codex-local-cli.d.ts +20 -0
- package/dist-server/harness/route/codex-local-cli.js +78 -0
- package/dist-server/harness/route/codex-local-manifest.d.ts +22 -0
- package/dist-server/harness/route/codex-local-manifest.js +180 -0
- package/dist-server/harness/route/describe.d.ts +54 -0
- package/dist-server/harness/route/describe.js +84 -0
- package/dist-server/harness/route/dispatched.d.ts +138 -0
- package/dist-server/harness/route/dispatched.js +339 -0
- package/dist-server/harness/route/in-process.d.ts +43 -0
- package/dist-server/harness/route/in-process.js +106 -0
- package/dist-server/harness/route/index.d.ts +19 -0
- package/dist-server/harness/route/index.js +12 -0
- package/dist-server/harness/route/profile-source.d.ts +37 -0
- package/dist-server/harness/route/profile-source.js +76 -0
- package/dist-server/harness/route/route.d.ts +33 -0
- package/dist-server/harness/route/route.js +114 -0
- package/dist-server/harness/route/tier-binding.d.ts +36 -0
- package/dist-server/harness/route/tier-binding.js +63 -0
- package/dist-server/harness/route/types.d.ts +286 -0
- package/dist-server/harness/route/types.js +1 -0
- package/dist-server/harness/session/errors.d.ts +4 -0
- package/dist-server/harness/session/errors.js +7 -0
- package/dist-server/harness/session/gate.d.ts +11 -0
- package/dist-server/harness/session/gate.js +13 -0
- package/dist-server/harness/session/index.d.ts +5 -0
- package/dist-server/harness/session/index.js +3 -0
- package/dist-server/harness/session/run.d.ts +60 -0
- package/dist-server/harness/session/run.js +333 -0
- package/dist-server/harness/session/types.d.ts +29 -0
- package/dist-server/harness/session/types.js +1 -0
- package/dist-server/harness/setup/codex-events.d.ts +12 -0
- package/dist-server/harness/setup/codex-events.js +108 -0
- package/dist-server/harness/setup/runner.d.ts +497 -0
- package/dist-server/harness/setup/runner.js +1314 -0
- package/dist-server/harness/tools/errors.d.ts +107 -0
- package/dist-server/harness/tools/errors.js +141 -0
- package/dist-server/harness/tools/index.d.ts +13 -0
- package/dist-server/harness/tools/index.js +8 -0
- package/dist-server/harness/tools/mcp.d.ts +26 -0
- package/dist-server/harness/tools/mcp.js +15 -0
- package/dist-server/harness/tools/native.d.ts +179 -0
- package/dist-server/harness/tools/native.js +322 -0
- package/dist-server/harness/tools/parse.d.ts +14 -0
- package/dist-server/harness/tools/parse.js +18 -0
- package/dist-server/harness/tools/resolve-wren-binary.d.ts +12 -0
- package/dist-server/harness/tools/resolve-wren-binary.js +21 -0
- package/dist-server/harness/tools/resolve.d.ts +51 -0
- package/dist-server/harness/tools/resolve.js +79 -0
- package/dist-server/harness/tools/setup-native.d.ts +84 -0
- package/dist-server/harness/tools/setup-native.js +353 -0
- package/dist-server/harness/tools/wiring.d.ts +10 -0
- package/dist-server/harness/tools/wiring.js +16 -0
- package/dist-server/providers/setup.provider.yaml +31 -0
- package/dist-server/providers/wren.provider.yaml +33 -0
- package/dist-server/server/adopt.d.ts +87 -0
- package/dist-server/server/adopt.js +263 -0
- package/dist-server/server/app.d.ts +6 -0
- package/dist-server/server/app.js +2521 -0
- package/dist-server/server/auth-choice.d.ts +16 -0
- package/dist-server/server/auth-choice.js +36 -0
- package/dist-server/server/bin.d.ts +2 -0
- package/dist-server/server/bin.js +601 -0
- package/dist-server/server/bootstrap-project-binding.d.ts +31 -0
- package/dist-server/server/bootstrap-project-binding.js +71 -0
- package/dist-server/server/clarify.d.ts +10 -0
- package/dist-server/server/clarify.js +17 -0
- package/dist-server/server/codex-setup-mcp.d.ts +2 -0
- package/dist-server/server/codex-setup-mcp.js +147 -0
- package/dist-server/server/compose.d.ts +89 -0
- package/dist-server/server/compose.js +271 -0
- package/dist-server/server/conn-config.d.ts +30 -0
- package/dist-server/server/conn-config.js +253 -0
- package/dist-server/server/context-files.d.ts +12 -0
- package/dist-server/server/context-files.js +117 -0
- package/dist-server/server/context-map.d.ts +30 -0
- package/dist-server/server/context-map.js +95 -0
- package/dist-server/server/context-source.d.ts +68 -0
- package/dist-server/server/context-source.js +85 -0
- package/dist-server/server/db.d.ts +862 -0
- package/dist-server/server/db.js +1839 -0
- package/dist-server/server/enrichment-runner.d.ts +184 -0
- package/dist-server/server/enrichment-runner.js +213 -0
- package/dist-server/server/enrichment-verify.d.ts +73 -0
- package/dist-server/server/enrichment-verify.js +290 -0
- package/dist-server/server/enrichment.d.ts +232 -0
- package/dist-server/server/enrichment.js +199 -0
- package/dist-server/server/env-detect.d.ts +8 -0
- package/dist-server/server/env-detect.js +17 -0
- package/dist-server/server/env-field-hints.d.ts +46 -0
- package/dist-server/server/env-field-hints.js +48 -0
- package/dist-server/server/fold.d.ts +133 -0
- package/dist-server/server/fold.js +407 -0
- package/dist-server/server/harness-profile-sources.d.ts +3 -0
- package/dist-server/server/harness-profile-sources.js +24 -0
- package/dist-server/server/harness.d.ts +19 -0
- package/dist-server/server/harness.js +439 -0
- package/dist-server/server/impact.d.ts +18 -0
- package/dist-server/server/impact.js +85 -0
- package/dist-server/server/interactive-terminal.d.ts +175 -0
- package/dist-server/server/interactive-terminal.js +336 -0
- package/dist-server/server/native-artifacts.d.ts +1004 -0
- package/dist-server/server/native-artifacts.js +564 -0
- package/dist-server/server/native-dispatch-registry.d.ts +42 -0
- package/dist-server/server/native-dispatch-registry.js +45 -0
- package/dist-server/server/native-producer-fence.d.ts +14 -0
- package/dist-server/server/native-producer-fence.js +48 -0
- package/dist-server/server/native-session-resume.d.ts +6 -0
- package/dist-server/server/native-session-resume.js +103 -0
- package/dist-server/server/native-session-workspace.d.ts +40 -0
- package/dist-server/server/native-session-workspace.js +175 -0
- package/dist-server/server/native-sessions.d.ts +374 -0
- package/dist-server/server/native-sessions.js +1517 -0
- package/dist-server/server/native-wren-runtime.d.ts +20 -0
- package/dist-server/server/native-wren-runtime.js +259 -0
- package/dist-server/server/node-pty-host.d.ts +31 -0
- package/dist-server/server/node-pty-host.js +100 -0
- package/dist-server/server/route-intent.d.ts +40 -0
- package/dist-server/server/route-intent.js +23 -0
- package/dist-server/server/runtime-binding.d.ts +52 -0
- package/dist-server/server/runtime-binding.js +258 -0
- package/dist-server/server/source-catalog.d.ts +84 -0
- package/dist-server/server/source-catalog.js +221 -0
- package/dist-server/server/source-driver.d.ts +35 -0
- package/dist-server/server/source-driver.js +124 -0
- package/dist-server/server/spa.d.ts +2 -0
- package/dist-server/server/spa.js +51 -0
- package/dist-server/server/subscription-model-catalog.d.ts +27 -0
- package/dist-server/server/subscription-model-catalog.js +203 -0
- package/dist-server/server/turn.d.ts +249 -0
- package/dist-server/server/turn.js +1074 -0
- package/dist-server/server/wire-types.d.ts +799 -0
- package/dist-server/server/wire-types.js +1 -0
- package/dist-server/server/wren-home.d.ts +19 -0
- package/dist-server/server/wren-home.js +33 -0
- package/dist-server/server/wren-profiles.d.ts +43 -0
- package/dist-server/server/wren-profiles.js +123 -0
- package/package.json +93 -0
- package/profiles/genbi-default/context/binding.yml +9 -0
- package/profiles/genbi-default/ir.golden.json +506 -0
- package/profiles/genbi-default/profile.yml +16 -0
- package/profiles/genbi-enrich-context/components/apply_enrichment/component.yml +42 -0
- package/profiles/genbi-enrich-context/components/draft_enrichment/component.yml +30 -0
- package/profiles/genbi-enrich-context/components/draft_enrichment/steps/draft.md +34 -0
- package/profiles/genbi-enrich-context/components/inspect_context/component.yml +32 -0
- package/profiles/genbi-enrich-context/components/inspect_context/steps/inspect.md +13 -0
- package/profiles/genbi-enrich-context/context/binding.yml +4 -0
- package/profiles/genbi-enrich-context/ir.golden.json +302 -0
- package/profiles/genbi-enrich-context/profile.yml +15 -0
- package/profiles/genbi-monitor/context/binding.yml +7 -0
- package/profiles/genbi-monitor/ir.golden.json +188 -0
- package/profiles/genbi-monitor/profile.yml +27 -0
- package/profiles/genbi-setup/components/build_context/component.yml +44 -0
- package/profiles/genbi-setup/components/build_context/steps/build.md +14 -0
- package/profiles/genbi-setup/components/connect_source/component.yml +51 -0
- package/profiles/genbi-setup/components/connect_source/steps/connect.md +23 -0
- package/profiles/genbi-setup/context/binding.yml +8 -0
- package/profiles/genbi-setup/ir.golden.json +140 -0
- package/profiles/genbi-setup/profile.yml +18 -0
- package/profiles/genbi-setup/raw/schema.json +5 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AgentEvent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Plain `Omit<AgentEvent, K>` does NOT distribute over the `AgentEvent`
|
|
4
|
+
* union — `Omit` is `Pick<T, Exclude<keyof T, K>>`, and `keyof` of a union
|
|
5
|
+
* only yields the keys common to every member, collapsing every variant-
|
|
6
|
+
* specific field (`stepId`, `tool`, `envelope`, ...) down to nothing.
|
|
7
|
+
* Distributing over each member first (via the naked `T extends any`
|
|
8
|
+
* conditional) preserves each variant's own fields and its `kind` literal.
|
|
9
|
+
*/
|
|
10
|
+
export type DistributiveOmit<T, K extends keyof any> = T extends unknown ? Omit<T, K> : never;
|
|
11
|
+
/**
|
|
12
|
+
* What an emission site (`runAgent`, `executeAgent`) actually knows: an
|
|
13
|
+
* `AgentEvent` minus the `runId`/`seq` bookkeeping that only the emitter
|
|
14
|
+
* that owns the run's counter can stamp.
|
|
15
|
+
*/
|
|
16
|
+
export type AgentEventInput = DistributiveOmit<AgentEvent, "runId" | "seq">;
|
|
17
|
+
export interface AgentEventEmitter {
|
|
18
|
+
readonly runId: string;
|
|
19
|
+
/** Stamps `runId`/`seq` onto `event` and forwards it to the sink. No-op (and never throws) when no sink was given. */
|
|
20
|
+
emit(event: AgentEventInput): void;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Owns one run's `runId` + monotonic `seq` counter, so two independent
|
|
24
|
+
* emission sites (`runAgent` for run/answer/refusal/run.finish/error;
|
|
25
|
+
* `executeAgent` for step/tool/artifact) can each describe *what happened*
|
|
26
|
+
* without needing to coordinate numbering themselves. `runAgent` is the
|
|
27
|
+
* natural sole owner: it creates one `AgentEventEmitter` per call and passes
|
|
28
|
+
* `emit` down as `ExecuteAgentContext.onEvent`.
|
|
29
|
+
*
|
|
30
|
+
* `sink` is the outward-facing seam (`RunAgentContext.onEvent`/
|
|
31
|
+
* `RouteOptions.onEvent`), typed `(e: AgentEvent) => void` exactly as the
|
|
32
|
+
* contract specifies — callers never see the bookkeeping split.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createAgentEventEmitter(sink: ((event: AgentEvent) => void) | undefined, runId?: string): AgentEventEmitter;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Owns one run's `runId` + monotonic `seq` counter, so two independent
|
|
4
|
+
* emission sites (`runAgent` for run/answer/refusal/run.finish/error;
|
|
5
|
+
* `executeAgent` for step/tool/artifact) can each describe *what happened*
|
|
6
|
+
* without needing to coordinate numbering themselves. `runAgent` is the
|
|
7
|
+
* natural sole owner: it creates one `AgentEventEmitter` per call and passes
|
|
8
|
+
* `emit` down as `ExecuteAgentContext.onEvent`.
|
|
9
|
+
*
|
|
10
|
+
* `sink` is the outward-facing seam (`RunAgentContext.onEvent`/
|
|
11
|
+
* `RouteOptions.onEvent`), typed `(e: AgentEvent) => void` exactly as the
|
|
12
|
+
* contract specifies — callers never see the bookkeeping split.
|
|
13
|
+
*/
|
|
14
|
+
export function createAgentEventEmitter(sink, runId = randomUUID()) {
|
|
15
|
+
let seq = 0;
|
|
16
|
+
return {
|
|
17
|
+
runId,
|
|
18
|
+
emit(event) {
|
|
19
|
+
if (!sink)
|
|
20
|
+
return;
|
|
21
|
+
seq += 1;
|
|
22
|
+
sink({ ...event, runId, seq });
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createAgentEventEmitter } from "./emitter.js";
|
|
2
|
+
export type { AgentEventEmitter, AgentEventInput } from "./emitter.js";
|
|
3
|
+
export { MalformedSseFrameError, parseAgentEvent, serializeAgentEvent } from "./sse.js";
|
|
4
|
+
export type { AgentEvent, AgentEventBase, AgentEventKind, AnswerAgentEvent, ArtifactAgentEvent, ArtifactKind, ErrorAgentEvent, RefusalAgentEvent, RunFinishEvent, RunStartEvent, StepFinishEvent, StepStartEvent, StepTrace, TokenEvent, ToolCallEvent, ToolResultEvent, TraceStep, } from "./types.js";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentEvent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Serializes one `AgentEvent` as a single `text/event-stream` frame per the
|
|
4
|
+
* harness→BFF wire format: `event: <kind>\ndata:
|
|
5
|
+
* <json>\n\n`. The harness itself never opens a socket — the BFF runs it
|
|
6
|
+
* In-process and consumes `AgentEvent`s directly via `onEvent` — this is
|
|
7
|
+
* only for the documented "if a future out-of-process split is wanted"
|
|
8
|
+
* fallback, defined + unit-tested here so it exists when needed.
|
|
9
|
+
*/
|
|
10
|
+
export declare function serializeAgentEvent(event: AgentEvent): string;
|
|
11
|
+
export declare class MalformedSseFrameError extends Error {
|
|
12
|
+
constructor(reason: string, frame: string);
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The round-trip counterpart to `serializeAgentEvent`. Not a general SSE-
|
|
16
|
+
* stream parser (no multi-frame buffering, no comment/retry-field support)
|
|
17
|
+
* — the wire format here is always exactly one `event:` line + one `data:`
|
|
18
|
+
* line per frame, so this only ever needs to split a single frame in two.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseAgentEvent(frame: string): AgentEvent;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serializes one `AgentEvent` as a single `text/event-stream` frame per the
|
|
3
|
+
* harness→BFF wire format: `event: <kind>\ndata:
|
|
4
|
+
* <json>\n\n`. The harness itself never opens a socket — the BFF runs it
|
|
5
|
+
* In-process and consumes `AgentEvent`s directly via `onEvent` — this is
|
|
6
|
+
* only for the documented "if a future out-of-process split is wanted"
|
|
7
|
+
* fallback, defined + unit-tested here so it exists when needed.
|
|
8
|
+
*/
|
|
9
|
+
export function serializeAgentEvent(event) {
|
|
10
|
+
return `event: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`;
|
|
11
|
+
}
|
|
12
|
+
export class MalformedSseFrameError extends Error {
|
|
13
|
+
constructor(reason, frame) {
|
|
14
|
+
super(`malformed SSE AgentEvent frame (${reason}): ${JSON.stringify(frame)}`);
|
|
15
|
+
this.name = "MalformedSseFrameError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The round-trip counterpart to `serializeAgentEvent`. Not a general SSE-
|
|
20
|
+
* stream parser (no multi-frame buffering, no comment/retry-field support)
|
|
21
|
+
* — the wire format here is always exactly one `event:` line + one `data:`
|
|
22
|
+
* line per frame, so this only ever needs to split a single frame in two.
|
|
23
|
+
*/
|
|
24
|
+
export function parseAgentEvent(frame) {
|
|
25
|
+
const trimmed = frame.endsWith("\n\n") ? frame.slice(0, -2) : frame;
|
|
26
|
+
const lines = trimmed.split("\n");
|
|
27
|
+
const eventLine = lines.find((line) => line.startsWith("event: "));
|
|
28
|
+
const dataLine = lines.find((line) => line.startsWith("data: "));
|
|
29
|
+
if (!eventLine || !dataLine) {
|
|
30
|
+
throw new MalformedSseFrameError("missing event: or data: line", frame);
|
|
31
|
+
}
|
|
32
|
+
const kind = eventLine.slice("event: ".length);
|
|
33
|
+
let parsed;
|
|
34
|
+
try {
|
|
35
|
+
parsed = JSON.parse(dataLine.slice("data: ".length));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new MalformedSseFrameError("data: line is not valid JSON", frame);
|
|
39
|
+
}
|
|
40
|
+
if (typeof parsed !== "object" || parsed === null || !("kind" in parsed)) {
|
|
41
|
+
throw new MalformedSseFrameError("data payload has no kind field", frame);
|
|
42
|
+
}
|
|
43
|
+
const data = parsed;
|
|
44
|
+
if (data.kind !== kind) {
|
|
45
|
+
throw new MalformedSseFrameError(`event: "${kind}" does not match data.kind "${data.kind}"`, frame);
|
|
46
|
+
}
|
|
47
|
+
return data;
|
|
48
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { RenderEnvelope } from "../render/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The mode-agnostic, back-end-emitted event union. Both in-process (the
|
|
4
|
+
* Vercel AI SDK) and dispatched (shelled `warble-agent-sdk chat`) emit this same
|
|
5
|
+
* shape; the BFF (out of scope here) folds it into the UI's own SSE wire
|
|
6
|
+
* frames.
|
|
7
|
+
*
|
|
8
|
+
* `parent`/`depth` (on `step.*`/`tool.*`) model sub-agent nesting. In-process has
|
|
9
|
+
* no Task mechanism — `executeAgent` runs a flat in-process DAG — so its
|
|
10
|
+
* events always carry `depth: 0` and never set `parent`. Only dispatched can
|
|
11
|
+
* populate them (this harness does not currently parse dispatched's own nesting;
|
|
12
|
+
* see `route/dispatched.ts`'s doc comment for the gap).
|
|
13
|
+
*/
|
|
14
|
+
export type AgentEventKind = "run.start" | "step.start" | "step.finish" | "tool.call" | "tool.result" | "token" | "answer" | "refusal" | "artifact" | "run.finish" | "error";
|
|
15
|
+
/** Every `AgentEvent` carries these — stamped by `createAgentEventEmitter`, never by an emission site itself. */
|
|
16
|
+
export interface AgentEventBase {
|
|
17
|
+
readonly runId: string;
|
|
18
|
+
readonly seq: number;
|
|
19
|
+
}
|
|
20
|
+
export interface RunStartEvent extends AgentEventBase {
|
|
21
|
+
readonly kind: "run.start";
|
|
22
|
+
readonly mode: "A" | "B";
|
|
23
|
+
readonly agentId: string;
|
|
24
|
+
}
|
|
25
|
+
export interface StepStartEvent extends AgentEventBase {
|
|
26
|
+
readonly kind: "step.start";
|
|
27
|
+
readonly stepId: string;
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly tier: string;
|
|
30
|
+
readonly parent?: string;
|
|
31
|
+
readonly depth: number;
|
|
32
|
+
}
|
|
33
|
+
export interface StepFinishEvent extends AgentEventBase {
|
|
34
|
+
readonly kind: "step.finish";
|
|
35
|
+
readonly stepId: string;
|
|
36
|
+
readonly name: string;
|
|
37
|
+
readonly status: "ok" | "error";
|
|
38
|
+
/**
|
|
39
|
+
* The step's own reasoning/output: on `"ok"` this is
|
|
40
|
+
* `summarizeToolOutput(text)` (the step's produced artifact text, bounded);
|
|
41
|
+
* on `"error"` it's the best available description of what went wrong.
|
|
42
|
+
* Optional/additive — a step.finish with no `detail` behaves exactly as it
|
|
43
|
+
* did before this field existed.
|
|
44
|
+
*/
|
|
45
|
+
readonly detail?: string;
|
|
46
|
+
}
|
|
47
|
+
export interface ToolCallEvent extends AgentEventBase {
|
|
48
|
+
readonly kind: "tool.call";
|
|
49
|
+
readonly stepId: string;
|
|
50
|
+
readonly callId: string;
|
|
51
|
+
readonly tool: string;
|
|
52
|
+
readonly input?: unknown;
|
|
53
|
+
readonly parent?: string;
|
|
54
|
+
readonly depth: number;
|
|
55
|
+
readonly status: "running";
|
|
56
|
+
}
|
|
57
|
+
export interface ToolResultEvent extends AgentEventBase {
|
|
58
|
+
readonly kind: "tool.result";
|
|
59
|
+
readonly stepId: string;
|
|
60
|
+
readonly callId: string;
|
|
61
|
+
readonly tool: string;
|
|
62
|
+
readonly status: "success" | "error";
|
|
63
|
+
/**
|
|
64
|
+
* Optional late-bound input for back-ends that only receive a safe tool
|
|
65
|
+
* payload when the call completes. LiveWorkLog merges this onto the
|
|
66
|
+
* matching tool.call entry before persisting the result.
|
|
67
|
+
*/
|
|
68
|
+
readonly input?: unknown;
|
|
69
|
+
readonly summary?: string;
|
|
70
|
+
readonly error?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface TokenEvent extends AgentEventBase {
|
|
73
|
+
readonly kind: "token";
|
|
74
|
+
readonly text: string;
|
|
75
|
+
}
|
|
76
|
+
/** `envelope` is in-process's shape (`RenderEnvelope`); `text` is dispatched's (`finalText`). Never both. */
|
|
77
|
+
export interface AnswerAgentEvent extends AgentEventBase {
|
|
78
|
+
readonly kind: "answer";
|
|
79
|
+
readonly envelope?: RenderEnvelope;
|
|
80
|
+
readonly text?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface RefusalAgentEvent extends AgentEventBase {
|
|
83
|
+
readonly kind: "refusal";
|
|
84
|
+
readonly reason: string;
|
|
85
|
+
readonly envelope?: RenderEnvelope;
|
|
86
|
+
}
|
|
87
|
+
export type ArtifactKind = "dashboard" | "report" | "chart";
|
|
88
|
+
export interface ArtifactAgentEvent extends AgentEventBase {
|
|
89
|
+
readonly kind: "artifact";
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly artifactKind: ArtifactKind;
|
|
92
|
+
readonly location: string;
|
|
93
|
+
}
|
|
94
|
+
export interface RunFinishEvent extends AgentEventBase {
|
|
95
|
+
readonly kind: "run.finish";
|
|
96
|
+
readonly status: "answer" | "refusal" | "error";
|
|
97
|
+
}
|
|
98
|
+
export interface ErrorAgentEvent extends AgentEventBase {
|
|
99
|
+
readonly kind: "error";
|
|
100
|
+
readonly message: string;
|
|
101
|
+
}
|
|
102
|
+
export type AgentEvent = RunStartEvent | StepStartEvent | StepFinishEvent | ToolCallEvent | ToolResultEvent | TokenEvent | AnswerAgentEvent | RefusalAgentEvent | ArtifactAgentEvent | RunFinishEvent | ErrorAgentEvent;
|
|
103
|
+
/**
|
|
104
|
+
* The minimal, deliberately-scoped trace floor. Built from the existing
|
|
105
|
+
* `onToolCallOutcome` seam with zero new emission infrastructure:
|
|
106
|
+
* terminal-only (no `running` state), flat (no `parent`/`depth`), ordered by
|
|
107
|
+
* fire order. See `runAgent`'s doc comment.
|
|
108
|
+
*/
|
|
109
|
+
export interface TraceStep {
|
|
110
|
+
readonly id: string;
|
|
111
|
+
readonly tool: string;
|
|
112
|
+
readonly outcome: "success" | "error";
|
|
113
|
+
readonly ordinal: number;
|
|
114
|
+
/**
|
|
115
|
+
* The tool call's input (e.g. the `query` tool's `{sql}`), so the UI's
|
|
116
|
+
* work-log can expand a step to show what it ran. Sourced straight from
|
|
117
|
+
* `ToolCallOutcome.input`; `unknown` because tool inputs are arbitrary
|
|
118
|
+
* per-tool shapes.
|
|
119
|
+
*/
|
|
120
|
+
readonly input?: unknown;
|
|
121
|
+
/**
|
|
122
|
+
* A compact, bounded human summary of what happened: on `"success"` this
|
|
123
|
+
* is `summarizeToolOutput(outcome.output)`; on `"error"` it's
|
|
124
|
+
* `outcome.error`. Never the full result payload — always short enough to
|
|
125
|
+
* render inline in a collapsed work-log row.
|
|
126
|
+
*/
|
|
127
|
+
readonly detail?: string;
|
|
128
|
+
}
|
|
129
|
+
export interface StepTrace {
|
|
130
|
+
readonly steps: readonly TraceStep[];
|
|
131
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Thrown when a requested path resolves outside its granted scope (directory traversal). */
|
|
2
|
+
export declare class PathTraversalError extends Error {
|
|
3
|
+
constructor(requestedPath: string, scope: string);
|
|
4
|
+
}
|
|
5
|
+
/** Thrown by `writeFile` when the policy grants no `artifactWriteScope` at all. */
|
|
6
|
+
export declare class WriteScopeNotGrantedError extends Error {
|
|
7
|
+
constructor(requestedPath: string);
|
|
8
|
+
}
|
|
9
|
+
/** Thrown by `exec` when a `mode: "write"` command runs under a read-only policy. */
|
|
10
|
+
export declare class ReadOnlyViolationError extends Error {
|
|
11
|
+
constructor(command: string);
|
|
12
|
+
}
|
|
13
|
+
/** Thrown by `fetch` when the target host isn't in the policy's egress allowlist. */
|
|
14
|
+
export declare class EgressNotAllowedError extends Error {
|
|
15
|
+
constructor(host: string);
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** Thrown when a requested path resolves outside its granted scope (directory traversal). */
|
|
2
|
+
export class PathTraversalError extends Error {
|
|
3
|
+
constructor(requestedPath, scope) {
|
|
4
|
+
super(`path "${requestedPath}" escapes the scoped workspace "${scope}"`);
|
|
5
|
+
this.name = "PathTraversalError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/** Thrown by `writeFile` when the policy grants no `artifactWriteScope` at all. */
|
|
9
|
+
export class WriteScopeNotGrantedError extends Error {
|
|
10
|
+
constructor(requestedPath) {
|
|
11
|
+
super(`write to "${requestedPath}" rejected: the enforcement policy grants no artifactWriteScope ` +
|
|
12
|
+
`(the agent has no locked scoped_write guardrail)`);
|
|
13
|
+
this.name = "WriteScopeNotGrantedError";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Thrown by `exec` when a `mode: "write"` command runs under a read-only policy. */
|
|
17
|
+
export class ReadOnlyViolationError extends Error {
|
|
18
|
+
constructor(command) {
|
|
19
|
+
super(`command "${command}" declares mode: "write" but the enforcement policy is read-only`);
|
|
20
|
+
this.name = "ReadOnlyViolationError";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/** Thrown by `fetch` when the target host isn't in the policy's egress allowlist. */
|
|
24
|
+
export class EgressNotAllowedError extends Error {
|
|
25
|
+
constructor(host) {
|
|
26
|
+
super(`fetch to host "${host}" is not in the egress allowlist`);
|
|
27
|
+
this.name = "EgressNotAllowedError";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { ExecCommand, ExecMode, ExecResult, ExecutionEnv, FetchRequest, FetchResponse } from "./types.js";
|
|
2
|
+
export type { ExecutionPolicy } from "./policy.js";
|
|
3
|
+
export { EgressNotAllowedError, PathTraversalError, ReadOnlyViolationError, WriteScopeNotGrantedError, } from "./errors.js";
|
|
4
|
+
export { createLocalExecutionEnv } from "./local.js";
|
|
5
|
+
export type { FetchImpl, LocalExecutionEnvOptions, RawFetchResponse } from "./local.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { ExecCommand, ExecResult, ExecutionEnv } from "./types.js";
|
|
2
|
+
/** The minimal shape `local.ts` needs from a fetch response — matches the global `Response`. */
|
|
3
|
+
export interface RawFetchResponse {
|
|
4
|
+
readonly status: number;
|
|
5
|
+
text(): Promise<string>;
|
|
6
|
+
}
|
|
7
|
+
export type FetchImpl = (url: string, init: {
|
|
8
|
+
readonly method: string;
|
|
9
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
10
|
+
readonly body?: string;
|
|
11
|
+
}) => Promise<RawFetchResponse>;
|
|
12
|
+
export interface LocalExecutionEnvOptions {
|
|
13
|
+
/** Base directory `artifactWriteScope` (and any relative path) resolves against. Defaults to `process.cwd()`. */
|
|
14
|
+
readonly rootDir?: string;
|
|
15
|
+
/** Overridable for hermetic tests; defaults to the global `fetch`. */
|
|
16
|
+
readonly fetchImpl?: FetchImpl;
|
|
17
|
+
/** Overridable for hermetic tests; defaults to spawning `cmd.command` with `cmd.args` (no shell). */
|
|
18
|
+
readonly execImpl?: (cmd: ExecCommand) => Promise<ExecResult>;
|
|
19
|
+
/** Overridable for hermetic tests; defaults to 32 MiB (matches compile/pipeline.ts + route/dispatched.ts). */
|
|
20
|
+
readonly maxBufferBytes?: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The v1 `local` `ExecutionEnv` backend: in-process file I/O confined
|
|
24
|
+
* to a scoped workspace, a no-shell subprocess runner gated by
|
|
25
|
+
* `policy.readOnly`, and a basic host allowlist for `fetch`. This is not a
|
|
26
|
+
* sandbox — it adds exactly the checks the guardrail mapping
|
|
27
|
+
* (`harness/guardrails/`) requires so a locked `read_only_execution` or
|
|
28
|
+
* `scoped_write` guardrail has real teeth here, not just prose in a prompt.
|
|
29
|
+
* No microVM/delegated/egress-proxy/credential-broker backend exists yet
|
|
30
|
+
* (out of scope for this milestone).
|
|
31
|
+
*/
|
|
32
|
+
export declare function createLocalExecutionEnv(options?: LocalExecutionEnvOptions): ExecutionEnv;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, readFile as readFileAsync, writeFile as writeFileAsync } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { EgressNotAllowedError, PathTraversalError, ReadOnlyViolationError, WriteScopeNotGrantedError } from "./errors.js";
|
|
5
|
+
const DEFAULT_MAX_BUFFER_BYTES = 32 * 1024 * 1024;
|
|
6
|
+
/**
|
|
7
|
+
* The v1 `local` `ExecutionEnv` backend: in-process file I/O confined
|
|
8
|
+
* to a scoped workspace, a no-shell subprocess runner gated by
|
|
9
|
+
* `policy.readOnly`, and a basic host allowlist for `fetch`. This is not a
|
|
10
|
+
* sandbox — it adds exactly the checks the guardrail mapping
|
|
11
|
+
* (`harness/guardrails/`) requires so a locked `read_only_execution` or
|
|
12
|
+
* `scoped_write` guardrail has real teeth here, not just prose in a prompt.
|
|
13
|
+
* No microVM/delegated/egress-proxy/credential-broker backend exists yet
|
|
14
|
+
* (out of scope for this milestone).
|
|
15
|
+
*/
|
|
16
|
+
export function createLocalExecutionEnv(options = {}) {
|
|
17
|
+
const rootDir = options.rootDir ?? process.cwd();
|
|
18
|
+
const fetchImpl = options.fetchImpl ?? ((url, init) => fetch(url, init));
|
|
19
|
+
const maxBufferBytes = options.maxBufferBytes ?? DEFAULT_MAX_BUFFER_BYTES;
|
|
20
|
+
const execImpl = options.execImpl ?? ((cmd) => runSubprocess(cmd, maxBufferBytes));
|
|
21
|
+
return {
|
|
22
|
+
async exec(cmd, policy) {
|
|
23
|
+
if (cmd.mode === "write" && policy.readOnly) {
|
|
24
|
+
throw new ReadOnlyViolationError(cmd.command);
|
|
25
|
+
}
|
|
26
|
+
return execImpl(cmd);
|
|
27
|
+
},
|
|
28
|
+
async readFile(filePath, policy) {
|
|
29
|
+
const resolved = resolveWithinScope(rootDir, policy.artifactWriteScope, filePath);
|
|
30
|
+
return readFileAsync(resolved, "utf-8");
|
|
31
|
+
},
|
|
32
|
+
async writeFile(filePath, data, policy) {
|
|
33
|
+
// read_only_execution and a locked scoped_write guardrail coexist by
|
|
34
|
+
// design (see EnforcementPolicy's doc comment) — writing the output
|
|
35
|
+
// artifact is gated purely on artifactWriteScope, never on readOnly.
|
|
36
|
+
if (policy.artifactWriteScope === undefined) {
|
|
37
|
+
throw new WriteScopeNotGrantedError(filePath);
|
|
38
|
+
}
|
|
39
|
+
const resolved = resolveWithinScope(rootDir, policy.artifactWriteScope, filePath);
|
|
40
|
+
await mkdir(path.dirname(resolved), { recursive: true });
|
|
41
|
+
await writeFileAsync(resolved, data, "utf-8");
|
|
42
|
+
},
|
|
43
|
+
async fetch(req, policy) {
|
|
44
|
+
const host = new URL(req.url).host;
|
|
45
|
+
const allowed = policy.allowedHosts ?? [];
|
|
46
|
+
if (!allowed.includes(host)) {
|
|
47
|
+
throw new EgressNotAllowedError(host);
|
|
48
|
+
}
|
|
49
|
+
const response = await fetchImpl(req.url, {
|
|
50
|
+
method: req.method ?? "GET",
|
|
51
|
+
...(req.headers ? { headers: req.headers } : {}),
|
|
52
|
+
...(req.body !== undefined ? { body: req.body } : {}),
|
|
53
|
+
});
|
|
54
|
+
const body = await response.text();
|
|
55
|
+
return { status: response.status, body };
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolves `requestedPath` against `scope` (itself resolved against
|
|
61
|
+
* `rootDir` when relative) and rejects anything that escapes it — the
|
|
62
|
+
* directory-traversal check `writeFile`/`readFile` both rely on. A
|
|
63
|
+
* `scope` of `undefined` confines reads to `rootDir` directly; `writeFile`
|
|
64
|
+
* rejects that case itself before ever reaching here.
|
|
65
|
+
*/
|
|
66
|
+
function resolveWithinScope(rootDir, scope, requestedPath) {
|
|
67
|
+
const scopeAbs = path.resolve(rootDir, scope ?? ".");
|
|
68
|
+
const targetAbs = path.resolve(scopeAbs, requestedPath);
|
|
69
|
+
const relative = path.relative(scopeAbs, targetAbs);
|
|
70
|
+
if (relative.startsWith("..") || path.isAbsolute(relative)) {
|
|
71
|
+
throw new PathTraversalError(requestedPath, scopeAbs);
|
|
72
|
+
}
|
|
73
|
+
return targetAbs;
|
|
74
|
+
}
|
|
75
|
+
function runSubprocess(cmd, maxBufferBytes) {
|
|
76
|
+
return new Promise((resolve) => {
|
|
77
|
+
const options = {
|
|
78
|
+
maxBuffer: maxBufferBytes,
|
|
79
|
+
...(cmd.cwd !== undefined ? { cwd: cmd.cwd } : {}),
|
|
80
|
+
...(cmd.timeoutMs !== undefined ? { timeout: cmd.timeoutMs } : {}),
|
|
81
|
+
};
|
|
82
|
+
const child = execFile(cmd.command, cmd.args ? [...cmd.args] : [], options, (error, stdout, stderr) => {
|
|
83
|
+
// A spawn failure (the binary isn't on PATH/doesn't exist)
|
|
84
|
+
// reports its error as `error.code === "ENOENT"` — a STRING. The old
|
|
85
|
+
// `typeof error.code === "number"` check treated that as "not a real
|
|
86
|
+
// exit code" and fell through to the generic `error ? 1 : 0`,
|
|
87
|
+
// flattening a missing-binary condition into an indistinguishable
|
|
88
|
+
// `exitCode: 1` with empty `stderr` (the process never ran, so it
|
|
89
|
+
// never wrote anything) — laundering a clear "not found" into what
|
|
90
|
+
// looks like an ordinary command failure.
|
|
91
|
+
if (error && error.code === "ENOENT") {
|
|
92
|
+
resolve({ stdout, stderr, exitCode: 1, notFound: true });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// guardrail-enforcement fix: `statementTimeoutSec` is only real
|
|
96
|
+
// enforcement if a hung `wren`/`warble` subprocess actually gets killed
|
|
97
|
+
// and reported distinguishably, rather than left to run forever. Node
|
|
98
|
+
// sets `error.killed === true` for its own `timeout` option; `stderr`
|
|
99
|
+
// is often empty in this case (the process was killed mid-run, not
|
|
100
|
+
// given a chance to explain itself), so a fallback message is supplied.
|
|
101
|
+
if (error && error.killed === true) {
|
|
102
|
+
resolve({
|
|
103
|
+
stdout,
|
|
104
|
+
stderr: stderr.length > 0 ? stderr : `subprocess "${cmd.command}" timed out after ${cmd.timeoutMs}ms`,
|
|
105
|
+
exitCode: 1,
|
|
106
|
+
timedOut: true,
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// Same rationale as above, for the other silent-killer: an oversized
|
|
111
|
+
// result set previously died on execFile's default 1 MiB `maxBuffer`
|
|
112
|
+
// as an opaque exit-1 with empty stderr, indistinguishable from any
|
|
113
|
+
// other failure.
|
|
114
|
+
if (error && error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
|
|
115
|
+
resolve({
|
|
116
|
+
stdout,
|
|
117
|
+
stderr: stderr.length > 0 ? stderr : `subprocess "${cmd.command}" exceeded the ${maxBufferBytes}-byte output buffer`,
|
|
118
|
+
exitCode: 1,
|
|
119
|
+
maxBufferExceeded: true,
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const exitCode = error && typeof error.code === "number" ? error.code : error ? 1 : 0;
|
|
124
|
+
resolve({ stdout, stderr, exitCode });
|
|
125
|
+
});
|
|
126
|
+
// ExecCommand has no stdin/input field: every command routed through this
|
|
127
|
+
// backend is deliberately non-interactive. Leaving execFile's writable
|
|
128
|
+
// stdin pipe open falsely promises that input may arrive later, so a bare
|
|
129
|
+
// interpreter or any other stdin reader waits forever. End it immediately
|
|
130
|
+
// to deliver EOF; if a future caller needs input, that must become an
|
|
131
|
+
// explicit ExecCommand contract which writes the payload and then ends.
|
|
132
|
+
child.stdin?.end();
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { EnforcementPolicy } from "../guardrails/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* The policy every `ExecutionEnv` method receives: the guardrail-derived
|
|
4
|
+
* `EnforcementPolicy` (`harness/guardrails/`) plus an egress allowlist. The
|
|
5
|
+
* allowlist has no bundle representation in v1 — the golden bundle never
|
|
6
|
+
* declares a host/dataset egress guardrail — so callers supply it directly
|
|
7
|
+
* (see `RunAgentContext.allowedHosts` in `harness/session/run.ts`). Omitting it
|
|
8
|
+
* means "no hosts allowed", the safe default.
|
|
9
|
+
*/
|
|
10
|
+
export interface ExecutionPolicy extends EnforcementPolicy {
|
|
11
|
+
readonly allowedHosts?: readonly string[];
|
|
12
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ExecutionPolicy } from "./policy.js";
|
|
2
|
+
/**
|
|
3
|
+
* A command's declared read/write intent. The harness does not parse or
|
|
4
|
+
* sniff shell commands to infer this — the caller (a native tool) states it
|
|
5
|
+
* up front, and a read-only policy rejects `"write"` commands outright.
|
|
6
|
+
*/
|
|
7
|
+
export type ExecMode = "read" | "write";
|
|
8
|
+
export interface ExecCommand {
|
|
9
|
+
readonly mode: ExecMode;
|
|
10
|
+
readonly command: string;
|
|
11
|
+
readonly args?: readonly string[];
|
|
12
|
+
/** Working directory the command runs in. Defaults to the process's cwd when omitted. */
|
|
13
|
+
readonly cwd?: string;
|
|
14
|
+
/** Maximum time (ms) the subprocess may run before being killed. Omitted means no timeout. */
|
|
15
|
+
readonly timeoutMs?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface ExecResult {
|
|
18
|
+
readonly stdout: string;
|
|
19
|
+
readonly stderr: string;
|
|
20
|
+
readonly exitCode: number;
|
|
21
|
+
/**
|
|
22
|
+
* Set (`true`) when `cmd.command` itself could not be launched — a spawn
|
|
23
|
+
* `ENOENT` (the binary isn't on `PATH`/doesn't exist), as opposed to the
|
|
24
|
+
* command running and exiting non-zero on its own. Node's spawn error
|
|
25
|
+
* carries this as the *string* `"ENOENT"` on `error.code`, not a number,
|
|
26
|
+
* so callers must check `notFound` rather than inferring it from
|
|
27
|
+
* `exitCode` — `exitCode` is set to `1` in this case only as a
|
|
28
|
+
* conservative "not success" fallback for code that ignores `notFound`,
|
|
29
|
+
* never as the real signal. Omitted (not just `false`) on an ordinary
|
|
30
|
+
* exit so existing structural equality checks on `ExecResult` are
|
|
31
|
+
* unaffected. See `createLocalExecutionEnv`'s `runSubprocess`.
|
|
32
|
+
*/
|
|
33
|
+
readonly notFound?: boolean;
|
|
34
|
+
/** Set (`true`) when the subprocess was killed because it exceeded `ExecCommand.timeoutMs`. Node's `execFile` sets `error.killed === true` for its own `timeout` option (and NOT for maxBuffer-exceeded — verified: `error.code` is `null` for timeout, not a number, not `"ENOENT"`). Omitted (not `false`) on an ordinary exit, same convention as `notFound`. */
|
|
35
|
+
readonly timedOut?: boolean;
|
|
36
|
+
/** Set (`true`) when the subprocess's combined stdout/stderr exceeded the exec backend's `maxBuffer` and was killed. Node reports this as `error.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER"` (verified on Node 23), distinct from both `notFound` and `timedOut`. Omitted (not `false`) on an ordinary exit. */
|
|
37
|
+
readonly maxBufferExceeded?: boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface FetchRequest {
|
|
40
|
+
readonly url: string;
|
|
41
|
+
readonly method?: string;
|
|
42
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
43
|
+
readonly body?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface FetchResponse {
|
|
46
|
+
readonly status: number;
|
|
47
|
+
readonly body: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The pluggable seam side-effecting native tools execute through instead of
|
|
51
|
+
* touching the filesystem/network/subprocess directly — mirroring the
|
|
52
|
+
* "injectable, defaulted" shape of `ProviderRegistry` and
|
|
53
|
+
* `NativeToolRegistry`. v1 ships exactly one backend
|
|
54
|
+
* ({@link createLocalExecutionEnv} in `./local.js`); no microVM / delegated /
|
|
55
|
+
* egress-proxy / credential-broker backend exists yet (out of scope for
|
|
56
|
+
* this milestone — tracked separately as hosted-runtime hardening).
|
|
57
|
+
*/
|
|
58
|
+
export interface ExecutionEnv {
|
|
59
|
+
exec(cmd: ExecCommand, policy: ExecutionPolicy): Promise<ExecResult>;
|
|
60
|
+
readFile(path: string, policy: ExecutionPolicy): Promise<string>;
|
|
61
|
+
writeFile(path: string, data: string, policy: ExecutionPolicy): Promise<void>;
|
|
62
|
+
fetch(req: FetchRequest, policy: ExecutionPolicy): Promise<FetchResponse>;
|
|
63
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { deriveEnforcement } from "./policy.js";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Agent } from "../bundle/schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* The enforcement policy derived from an agent's `guardrails` map: a
|
|
4
|
+
* machine-readable summary an `ExecutionEnv`
|
|
5
|
+
* (`harness/exec/`) can act on directly. Deliberately excludes `gated_check`
|
|
6
|
+
* (`deterministic_gate` / `additivity_guard`) — `harness/session/gate.ts`
|
|
7
|
+
* already owns that guardrail's enforcement against the rendered envelope,
|
|
8
|
+
* and duplicating it here would just be two places to keep in sync.
|
|
9
|
+
*
|
|
10
|
+
* For `mcp:<server>/<name>` tools (e.g. `query`), the harness never executes
|
|
11
|
+
* the tool in-process — the MCP server does — so `rowLimit` /
|
|
12
|
+
* `statementTimeoutSec` (and `readOnly` for SQL access) are *not* enforced
|
|
13
|
+
* by this policy directly. They must be passed to the MCP server via its
|
|
14
|
+
* connection config (`McpServerConfig`, `harness/tools/mcp.ts`) or honored by
|
|
15
|
+
* the server's own semantics (e.g. a read-only DB role, a statement
|
|
16
|
+
* timeout on the connection). This module only derives the values; wiring
|
|
17
|
+
* them into a real server's connection is a caller/deployment concern this
|
|
18
|
+
* harness does not — and should not pretend to — enforce on the server's
|
|
19
|
+
* behalf.
|
|
20
|
+
*/
|
|
21
|
+
export interface EnforcementPolicy {
|
|
22
|
+
readonly readOnly: boolean;
|
|
23
|
+
readonly artifactWriteScope?: string;
|
|
24
|
+
readonly rowLimit?: number;
|
|
25
|
+
readonly statementTimeoutSec?: number;
|
|
26
|
+
readonly drillDepthLimit?: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Maps an agent's `guardrails` to the harness's enforcement seams: a locked
|
|
30
|
+
* `read_only` guardrail sets `readOnly`; a locked `scoped_write` guardrail
|
|
31
|
+
* (with a `scope`) sets `artifactWriteScope`; `threshold_limit` / `generic`
|
|
32
|
+
* guardrails are matched by guardrail *key* (not just `enforcement`, since
|
|
33
|
+
* `row_limit` and `drill_depth_limit` share the `threshold_limit`
|
|
34
|
+
* enforcement kind) onto named numeric limits.
|
|
35
|
+
*
|
|
36
|
+
* `read_only_execution` and a locked `scoped_write` guardrail coexisting
|
|
37
|
+
* (`generate_dashboard`, `explain_change` in the golden bundle) is not a
|
|
38
|
+
* contradiction: the former restricts *data access* (no write-capable
|
|
39
|
+
* SQL/tool becomes active), the latter grants a narrow, separate permission
|
|
40
|
+
* to write the rendered *output artifact* inside `scope`. This is exactly
|
|
41
|
+
* why the two fields are independent here rather than one overriding the
|
|
42
|
+
* other — `LocalExecutionEnv.writeFile` (`harness/exec/local.ts`) reflects this
|
|
43
|
+
* directly, consulting only `artifactWriteScope`, never `readOnly`.
|
|
44
|
+
*/
|
|
45
|
+
export declare function deriveEnforcement(agent: Agent): EnforcementPolicy;
|