@zeroroot-ai/zerocool 0.8.0 → 0.9.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/dist/dispatch.d.ts +69 -74
- package/dist/dispatch.js +77 -129
- package/dist/findings.d.ts +21 -2
- package/dist/findings.js +3 -80
- package/dist/fix.js +2 -0
- package/dist/gitlab.js +2 -0
- package/dist/http-probe.js +2 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +39 -39
- package/dist/knowledge-source.d.ts +34 -9
- package/dist/knowledge-source.js +33 -12
- package/dist/knowledge.d.ts +0 -9
- package/dist/knowledge.js +3 -53
- package/dist/mcp-config.d.ts +20 -0
- package/dist/mcp-config.js +31 -0
- package/dist/opencode-run.js +2 -0
- package/dist/semgrep.js +2 -0
- package/dist/serve-agent.js +2 -0
- package/dist/serve.js +2 -0
- package/dist/source-analysis.js +2 -0
- package/dist/watch.js +2 -0
- package/package.json +5 -5
- package/dist/componentize.d.ts +0 -10
- package/dist/componentize.js +0 -149
- package/dist/delegate.d.ts +0 -11
- package/dist/delegate.js +0 -216
- package/dist/gibson-tools.d.ts +0 -45
- package/dist/gibson-tools.js +0 -175
package/dist/dispatch.d.ts
CHANGED
|
@@ -33,18 +33,21 @@
|
|
|
33
33
|
* result. stdout is therefore a clean event stream — the driver's own logs go to
|
|
34
34
|
* stderr — and a clean exit lets the sandbox tear down.
|
|
35
35
|
*
|
|
36
|
-
*
|
|
36
|
+
* THE LAUNCHER CONTRACT IS READ ONCE, BY THE SDK. `readSandboxDispatch` from
|
|
37
|
+
* `@zeroroot-ai/sdk` is the one reader of the launch environment in this
|
|
38
|
+
* package, and `sandboxHarness` opens the callbacks on what it read. The
|
|
39
|
+
* launcher names — GIBSON_CG_JWT, GIBSON_CALLBACK_ENDPOINT,
|
|
40
|
+
* GIBSON_AGENT_TASK_B64, GIBSON_MISSION_ID, GIBSON_MISSION_RUN_ID,
|
|
41
|
+
* GIBSON_AGENT_RUN_ID, GIBSON_MODEL, GIBSON_TRACE_ID, GIBSON_SPAN_ID — are
|
|
42
|
+
* spelled in the SDK, against gibson `internal/engine/harness/sandboxed/agent.go`
|
|
43
|
+
* (the `envAgent*` constants). This file spells none of them. A second spelling
|
|
44
|
+
* here is how the two drift, which is the defect zerocool-plugins#7 records.
|
|
37
45
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
46
|
+
* FOUR VARIABLES ARE STILL READ HERE, and every one of them is a local option
|
|
47
|
+
* of this host rather than part of the launcher contract. The launcher never
|
|
48
|
+
* writes any of them, so the SDK carries none of them:
|
|
41
49
|
*
|
|
42
|
-
* GIBSON_CALLBACK_ENDPOINT per-dispatch harness callback dial target (required)
|
|
43
|
-
* GIBSON_CG_JWT per-dispatch capability grant (CG-JWT) (required)
|
|
44
|
-
* GIBSON_AGENT_TASK_B64 base64 protojson gibson.types.v1.Task (required)
|
|
45
|
-
* GIBSON_MODEL provider/model resolved for the tenant at dispatch
|
|
46
50
|
* GIBSON_CALLBACK_INSECURE "1" to dial the callback listener without TLS (kind)
|
|
47
|
-
* GIBSON_MISSION_ID / GIBSON_MISSION_RUN_ID / GIBSON_AGENT_RUN_ID provenance
|
|
48
51
|
* ZEROCOOL_WORKSPACE working directory (defaults to the cwd)
|
|
49
52
|
* ZEROCOOL_TASK force a task kind ("source-analysis", "watch",
|
|
50
53
|
* "fix"); the Task's own context `zerocool.task`
|
|
@@ -54,9 +57,9 @@
|
|
|
54
57
|
*
|
|
55
58
|
* The child `opencode run` still receives the grant as GIBSON_CALLBACK_TOKEN via
|
|
56
59
|
* {@link dispatchChildEnv} — that is the opencode plugin's own contract, fed from
|
|
57
|
-
* the
|
|
60
|
+
* the grant the SDK read.
|
|
58
61
|
*/
|
|
59
|
-
import { type AgentOutcome, type TaskHarness } from "@zeroroot-ai/sdk";
|
|
62
|
+
import { type AgentOutcome, type SandboxDispatch, type TaskHarness } from "@zeroroot-ai/sdk";
|
|
60
63
|
import { type FindingsBackend } from "./findings.js";
|
|
61
64
|
import { type FindingSource, type FindingStatusWriter, type FixPlanner, type Workspace } from "./fix.js";
|
|
62
65
|
import { type GitLabClient, type GitLabWriter } from "./gitlab.js";
|
|
@@ -65,8 +68,11 @@ import type { SemgrepRunner } from "./semgrep.js";
|
|
|
65
68
|
import { type ScanLauncher, type WatchCheckpoints } from "./watch.js";
|
|
66
69
|
import { type TriageModel } from "./source-analysis.js";
|
|
67
70
|
/**
|
|
68
|
-
* The per-dispatch grant
|
|
69
|
-
*
|
|
71
|
+
* The per-dispatch grant as the OPENCODE CHILD receives it.
|
|
72
|
+
*
|
|
73
|
+
* These are `ExecuteRequest` field names, not launcher environment names. The
|
|
74
|
+
* shape is shared with `serve-agent.ts`, which fills it from a polled work
|
|
75
|
+
* item, so the child sees one environment however the run was launched.
|
|
70
76
|
*/
|
|
71
77
|
export interface CallbackGrant {
|
|
72
78
|
/** `ExecuteRequest.callback_endpoint` — a bare `host:port` or an http(s) URL. */
|
|
@@ -80,27 +86,6 @@ export interface CallbackGrant {
|
|
|
80
86
|
agentRunId?: string;
|
|
81
87
|
traceId?: string;
|
|
82
88
|
}
|
|
83
|
-
/** Everything one sandboxed dispatch needs, read from the environment. */
|
|
84
|
-
export interface DispatchContext extends CallbackGrant {
|
|
85
|
-
/** `task.goal` — the natural-language objective the sandbox handed over. */
|
|
86
|
-
goal: string;
|
|
87
|
-
/**
|
|
88
|
-
* `task.context` — the string map the mission node set. Selects the task kind
|
|
89
|
-
* (`zerocool.task`) and carries what a task needs beyond a goal: the
|
|
90
|
-
* repository url and commit, a sub-path, the target id.
|
|
91
|
-
*/
|
|
92
|
-
taskContext: Record<string, string>;
|
|
93
|
-
/** `GIBSON_MISSION_ID`, so a Finding names the mission it belongs to. */
|
|
94
|
-
missionId?: string;
|
|
95
|
-
/** opencode edits files here; it is the run's workspace. */
|
|
96
|
-
workspace: string;
|
|
97
|
-
/** `provider/model`, e.g. `gibson/default`. Omitted lets opencode choose. */
|
|
98
|
-
model?: string;
|
|
99
|
-
/** Continue an earlier opencode session instead of starting a new one. */
|
|
100
|
-
sessionId?: string;
|
|
101
|
-
/** Hard deadline for the run, in ms. `0` when none was set. */
|
|
102
|
-
timeoutMs: number;
|
|
103
|
-
}
|
|
104
89
|
/**
|
|
105
90
|
* Render the per-dispatch grant back to environment for the opencode child, so
|
|
106
91
|
* the child's harness callbacks travel on the task grant rather than on any
|
|
@@ -112,46 +97,45 @@ export interface DispatchContext extends CallbackGrant {
|
|
|
112
97
|
*/
|
|
113
98
|
export declare function dispatchChildEnv(g: CallbackGrant): NodeJS.ProcessEnv;
|
|
114
99
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
100
|
+
* Everything one sandboxed dispatch needs.
|
|
101
|
+
*
|
|
102
|
+
* The launcher contract, exactly as `readSandboxDispatch` read it, plus the
|
|
103
|
+
* local options of this host. Extending {@link SandboxDispatch} rather than
|
|
104
|
+
* copying its fields keeps one name for each thing: the grant is `grant`, the
|
|
105
|
+
* dial target is `callbackEndpoint`, and there is no second spelling to drift.
|
|
119
106
|
*/
|
|
120
|
-
export
|
|
121
|
-
/**
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
107
|
+
export interface DispatchContext extends SandboxDispatch {
|
|
108
|
+
/**
|
|
109
|
+
* `task.context` read as strings. The mission node sets it. It selects the
|
|
110
|
+
* task kind (`zerocool.task`) and carries what a task needs beyond a goal:
|
|
111
|
+
* the repository url and commit, a sub-path, the target id.
|
|
112
|
+
*/
|
|
113
|
+
taskContext: Record<string, string>;
|
|
114
|
+
/** Dial the callback listener without TLS. A local option, not the contract. */
|
|
115
|
+
insecure: boolean;
|
|
116
|
+
/** opencode edits files here; it is the run's workspace. */
|
|
117
|
+
workspace: string;
|
|
118
|
+
/** Continue an earlier opencode session instead of starting a new one. */
|
|
119
|
+
sessionId?: string;
|
|
120
|
+
/** Hard deadline for the run, in ms. `0` when none was set. */
|
|
121
|
+
timeoutMs: number;
|
|
126
122
|
}
|
|
127
123
|
/**
|
|
128
|
-
*
|
|
129
|
-
* maps a mission node sets. protojson renders both as plain objects, so a
|
|
130
|
-
* value that is not a string is dropped rather than guessed at.
|
|
131
|
-
*/
|
|
132
|
-
export declare function decodeTask(taskB64: string | undefined): DecodedTask;
|
|
133
|
-
/**
|
|
134
|
-
* Read one `Task.context` / `Task.metadata` entry as a string.
|
|
135
|
-
*
|
|
136
|
-
* Both maps are `map<string, gibson.common.v1.TypedValue>`, so canonical
|
|
137
|
-
* protojson renders every value as a one-key object naming the oneof arm —
|
|
138
|
-
* `{"stringValue":"source-analysis"}`, `{"intValue":"42"}` — never as a bare
|
|
139
|
-
* string. gibson marshals the Task with `protojson.Marshal` over
|
|
140
|
-
* `agent.TaskToProto`, which wraps each entry with `mapToTypedValueMap`, so
|
|
141
|
-
* this is the only shape a dispatched agent ever receives. Reading these maps
|
|
142
|
-
* as plain strings silently dropped every entry, which is why a mission node's
|
|
143
|
-
* `zerocool.task`, `repository.commit` and `target.id` never reached the run.
|
|
124
|
+
* Read one `Task.context` entry as a string.
|
|
144
125
|
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
126
|
+
* `Task.context` is `map<string, gibson.common.v1.TypedValue>`. The SDK parses
|
|
127
|
+
* the launcher's protojson with the generated schema, so every value arrives
|
|
128
|
+
* as the protobuf-es oneof shape and never as a bare string. Reading these
|
|
129
|
+
* maps as plain strings dropped every entry, which is why a mission node's
|
|
130
|
+
* `zerocool.task`, `repository.commit` and `target.id` never reached a run.
|
|
150
131
|
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
132
|
+
* A null yields no entry: a null is the absence of a value, and rendering it
|
|
133
|
+
* would hand a task a target id of `"null"`. Bytes, arrays and maps yield no
|
|
134
|
+
* entry either — a task reads these keys as single strings.
|
|
153
135
|
*/
|
|
154
|
-
export declare function typedValueString(
|
|
136
|
+
export declare function typedValueString(v: SandboxDispatch["task"]["context"][string]): string | undefined;
|
|
137
|
+
/** The whole context map as strings. Entries with no readable value are dropped. */
|
|
138
|
+
export declare function taskContextStrings(context: SandboxDispatch["task"]["context"]): Record<string, string>;
|
|
155
139
|
/** The task kinds a sandboxed dispatch can serve. */
|
|
156
140
|
export type DispatchTaskKind = "opencode" | "source-analysis" | "watch" | "fix";
|
|
157
141
|
/**
|
|
@@ -161,21 +145,32 @@ export type DispatchTaskKind = "opencode" | "source-analysis" | "watch" | "fix";
|
|
|
161
145
|
*/
|
|
162
146
|
export declare function dispatchTaskKind(ctx: Pick<DispatchContext, "taskContext">, env?: NodeJS.ProcessEnv): DispatchTaskKind;
|
|
163
147
|
/**
|
|
164
|
-
* Read one sandboxed dispatch
|
|
148
|
+
* Read one sandboxed dispatch from the environment.
|
|
149
|
+
*
|
|
150
|
+
* The launcher contract comes from `readSandboxDispatch`, which is the one
|
|
151
|
+
* reader of those names on the TypeScript side. It fails, never falls back: a
|
|
152
|
+
* launch without a grant, an endpoint or a task is a defect in the launch, not
|
|
153
|
+
* something to guess around by running opencode with no task or by making
|
|
154
|
+
* unauthenticated calls.
|
|
165
155
|
*
|
|
166
156
|
* Deliberately never reads `GIBSON_BOOTSTRAP_TOKEN` or a host key: a sandboxed
|
|
167
157
|
* run is authenticated by its per-dispatch grant alone, and reaching for a
|
|
168
158
|
* bootstrap token here would reintroduce the enrollment handshake this shape
|
|
169
159
|
* exists to avoid.
|
|
170
160
|
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
161
|
+
* What this adds on top are this host's own options — the workspace, the
|
|
162
|
+
* deadline, the opencode session to continue, and whether to dial the callback
|
|
163
|
+
* listener without TLS. The launcher writes none of them, so the SDK carries
|
|
164
|
+
* none of them, and reading them here is not a second spelling of the contract.
|
|
175
165
|
*/
|
|
176
166
|
export declare function readDispatchContext(env: NodeJS.ProcessEnv, opts?: {
|
|
177
167
|
cwd?: string;
|
|
178
168
|
}): DispatchContext;
|
|
169
|
+
/**
|
|
170
|
+
* The task harness for this dispatch: the grant the launch injected, nothing
|
|
171
|
+
* else. `sandboxHarness` is the SDK's opener for exactly this shape.
|
|
172
|
+
*/
|
|
173
|
+
export declare function dispatchHarness(ctx: DispatchContext): TaskHarness;
|
|
179
174
|
/** Injectable seams, so a test drives a dispatch without spawning opencode. */
|
|
180
175
|
export interface DispatchDeps {
|
|
181
176
|
/** Overridable opencode driver. */
|
|
@@ -186,7 +181,7 @@ export interface DispatchDeps {
|
|
|
186
181
|
semgrep?: SemgrepRunner;
|
|
187
182
|
model?: TriageModel;
|
|
188
183
|
findings?: FindingsBackend;
|
|
189
|
-
/** Opens the task harness. Defaults to
|
|
184
|
+
/** Opens the task harness. Defaults to {@link dispatchHarness} on the dispatch grant. */
|
|
190
185
|
harness?: (ctx: DispatchContext) => TaskHarness;
|
|
191
186
|
/** Force the task kind, over the Task's own context. */
|
|
192
187
|
taskKind?: DispatchTaskKind;
|
package/dist/dispatch.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: Elastic-2.0
|
|
3
|
+
// Copyright 2026 Zero Root AI
|
|
2
4
|
/**
|
|
3
5
|
* zerocool-dispatch — one sandboxed dispatched run, then exit.
|
|
4
6
|
*
|
|
@@ -33,18 +35,21 @@
|
|
|
33
35
|
* result. stdout is therefore a clean event stream — the driver's own logs go to
|
|
34
36
|
* stderr — and a clean exit lets the sandbox tear down.
|
|
35
37
|
*
|
|
36
|
-
*
|
|
38
|
+
* THE LAUNCHER CONTRACT IS READ ONCE, BY THE SDK. `readSandboxDispatch` from
|
|
39
|
+
* `@zeroroot-ai/sdk` is the one reader of the launch environment in this
|
|
40
|
+
* package, and `sandboxHarness` opens the callbacks on what it read. The
|
|
41
|
+
* launcher names — GIBSON_CG_JWT, GIBSON_CALLBACK_ENDPOINT,
|
|
42
|
+
* GIBSON_AGENT_TASK_B64, GIBSON_MISSION_ID, GIBSON_MISSION_RUN_ID,
|
|
43
|
+
* GIBSON_AGENT_RUN_ID, GIBSON_MODEL, GIBSON_TRACE_ID, GIBSON_SPAN_ID — are
|
|
44
|
+
* spelled in the SDK, against gibson `internal/engine/harness/sandboxed/agent.go`
|
|
45
|
+
* (the `envAgent*` constants). This file spells none of them. A second spelling
|
|
46
|
+
* here is how the two drift, which is the defect zerocool-plugins#7 records.
|
|
37
47
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
48
|
+
* FOUR VARIABLES ARE STILL READ HERE, and every one of them is a local option
|
|
49
|
+
* of this host rather than part of the launcher contract. The launcher never
|
|
50
|
+
* writes any of them, so the SDK carries none of them:
|
|
41
51
|
*
|
|
42
|
-
* GIBSON_CALLBACK_ENDPOINT per-dispatch harness callback dial target (required)
|
|
43
|
-
* GIBSON_CG_JWT per-dispatch capability grant (CG-JWT) (required)
|
|
44
|
-
* GIBSON_AGENT_TASK_B64 base64 protojson gibson.types.v1.Task (required)
|
|
45
|
-
* GIBSON_MODEL provider/model resolved for the tenant at dispatch
|
|
46
52
|
* GIBSON_CALLBACK_INSECURE "1" to dial the callback listener without TLS (kind)
|
|
47
|
-
* GIBSON_MISSION_ID / GIBSON_MISSION_RUN_ID / GIBSON_AGENT_RUN_ID provenance
|
|
48
53
|
* ZEROCOOL_WORKSPACE working directory (defaults to the cwd)
|
|
49
54
|
* ZEROCOOL_TASK force a task kind ("source-analysis", "watch",
|
|
50
55
|
* "fix"); the Task's own context `zerocool.task`
|
|
@@ -54,9 +59,9 @@
|
|
|
54
59
|
*
|
|
55
60
|
* The child `opencode run` still receives the grant as GIBSON_CALLBACK_TOKEN via
|
|
56
61
|
* {@link dispatchChildEnv} — that is the opencode plugin's own contract, fed from
|
|
57
|
-
* the
|
|
62
|
+
* the grant the SDK read.
|
|
58
63
|
*/
|
|
59
|
-
import { observe,
|
|
64
|
+
import { observe, readSandboxDispatch, sandboxHarness, taskKnowledge, } from "@zeroroot-ai/sdk";
|
|
60
65
|
import { join } from "node:path";
|
|
61
66
|
import { taskFindingsBackend } from "./findings.js";
|
|
62
67
|
import { harnessFindingSource, harnessFindingStatus, runFix, } from "./fix.js";
|
|
@@ -84,96 +89,42 @@ export function dispatchChildEnv(g) {
|
|
|
84
89
|
};
|
|
85
90
|
}
|
|
86
91
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
* value that is not a string is dropped rather than guessed at.
|
|
92
|
+
* Read one `Task.context` entry as a string.
|
|
93
|
+
*
|
|
94
|
+
* `Task.context` is `map<string, gibson.common.v1.TypedValue>`. The SDK parses
|
|
95
|
+
* the launcher's protojson with the generated schema, so every value arrives
|
|
96
|
+
* as the protobuf-es oneof shape and never as a bare string. Reading these
|
|
97
|
+
* maps as plain strings dropped every entry, which is why a mission node's
|
|
98
|
+
* `zerocool.task`, `repository.commit` and `target.id` never reached a run.
|
|
99
|
+
*
|
|
100
|
+
* A null yields no entry: a null is the absence of a value, and rendering it
|
|
101
|
+
* would hand a task a target id of `"null"`. Bytes, arrays and maps yield no
|
|
102
|
+
* entry either — a task reads these keys as single strings.
|
|
99
103
|
*/
|
|
100
|
-
export function
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
+
export function typedValueString(v) {
|
|
105
|
+
switch (v.kind.case) {
|
|
106
|
+
case "stringValue":
|
|
107
|
+
return v.kind.value;
|
|
108
|
+
case "intValue":
|
|
109
|
+
return String(v.kind.value);
|
|
110
|
+
case "doubleValue":
|
|
111
|
+
return String(v.kind.value);
|
|
112
|
+
case "boolValue":
|
|
113
|
+
return String(v.kind.value);
|
|
114
|
+
default:
|
|
115
|
+
return undefined;
|
|
104
116
|
}
|
|
105
|
-
let task;
|
|
106
|
-
try {
|
|
107
|
-
task = JSON.parse(Buffer.from(taskB64, "base64").toString("utf8"));
|
|
108
|
-
}
|
|
109
|
-
catch (e) {
|
|
110
|
-
throw new Error("GIBSON_AGENT_TASK_B64 is not valid base64 protojson of a gibson.types.v1.Task: " +
|
|
111
|
-
(e instanceof Error ? e.message : String(e)));
|
|
112
|
-
}
|
|
113
|
-
const goal = typeof task.goal === "string" ? task.goal.trim() : "";
|
|
114
|
-
if (!goal) {
|
|
115
|
-
throw new Error("GIBSON_AGENT_TASK_B64 decoded to a Task with no goal. Running opencode against an empty " +
|
|
116
|
-
"prompt would report whatever it says back as a mission result.");
|
|
117
|
-
}
|
|
118
|
-
return { goal, context: stringMap(task.context), metadata: stringMap(task.metadata) };
|
|
119
117
|
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
return {};
|
|
118
|
+
/** The whole context map as strings. Entries with no readable value are dropped. */
|
|
119
|
+
export function taskContextStrings(context) {
|
|
123
120
|
const out = {};
|
|
124
|
-
for (const [k,
|
|
125
|
-
const s = typedValueString(
|
|
121
|
+
for (const [k, v] of Object.entries(context ?? {})) {
|
|
122
|
+
const s = typedValueString(v);
|
|
126
123
|
if (s !== undefined)
|
|
127
124
|
out[k] = s;
|
|
128
125
|
}
|
|
129
126
|
return out;
|
|
130
127
|
}
|
|
131
|
-
/**
|
|
132
|
-
* Read one `Task.context` / `Task.metadata` entry as a string.
|
|
133
|
-
*
|
|
134
|
-
* Both maps are `map<string, gibson.common.v1.TypedValue>`, so canonical
|
|
135
|
-
* protojson renders every value as a one-key object naming the oneof arm —
|
|
136
|
-
* `{"stringValue":"source-analysis"}`, `{"intValue":"42"}` — never as a bare
|
|
137
|
-
* string. gibson marshals the Task with `protojson.Marshal` over
|
|
138
|
-
* `agent.TaskToProto`, which wraps each entry with `mapToTypedValueMap`, so
|
|
139
|
-
* this is the only shape a dispatched agent ever receives. Reading these maps
|
|
140
|
-
* as plain strings silently dropped every entry, which is why a mission node's
|
|
141
|
-
* `zerocool.task`, `repository.commit` and `target.id` never reached the run.
|
|
142
|
-
*
|
|
143
|
-
* A bare string is still accepted, because a hand-built Task written as plain
|
|
144
|
-
* JSON is the normal way to drive a dispatch by hand. Both snake_case and
|
|
145
|
-
* lowerCamel arm names are read: protojson emits lowerCamel by default but can
|
|
146
|
-
* be told to keep the proto field names, and a decoder that accepts only one of
|
|
147
|
-
* them fails on a Task that is otherwise valid.
|
|
148
|
-
*
|
|
149
|
-
* `nullValue` yields no entry: a null is the absence of a value, and rendering
|
|
150
|
-
* it as the string "null" would hand a task a target id of `"null"`.
|
|
151
|
-
*/
|
|
152
|
-
export function typedValueString(val) {
|
|
153
|
-
if (typeof val === "string")
|
|
154
|
-
return val;
|
|
155
|
-
if (!val || typeof val !== "object")
|
|
156
|
-
return undefined;
|
|
157
|
-
const v = val;
|
|
158
|
-
const arm = (camel, snake) => (v[camel] !== undefined ? v[camel] : v[snake]);
|
|
159
|
-
const s = arm("stringValue", "string_value");
|
|
160
|
-
if (typeof s === "string")
|
|
161
|
-
return s;
|
|
162
|
-
// protojson renders int64 and uint64 as strings, and double as a number.
|
|
163
|
-
const i = arm("intValue", "int_value");
|
|
164
|
-
if (typeof i === "string" || typeof i === "number")
|
|
165
|
-
return String(i);
|
|
166
|
-
const u = arm("uintValue", "uint_value");
|
|
167
|
-
if (typeof u === "string" || typeof u === "number")
|
|
168
|
-
return String(u);
|
|
169
|
-
const d = arm("doubleValue", "double_value");
|
|
170
|
-
if (typeof d === "number")
|
|
171
|
-
return String(d);
|
|
172
|
-
const b = arm("boolValue", "bool_value");
|
|
173
|
-
if (typeof b === "boolean")
|
|
174
|
-
return String(b);
|
|
175
|
-
return undefined;
|
|
176
|
-
}
|
|
177
128
|
/**
|
|
178
129
|
* Which task this dispatch runs. The mission node says so in the Task's
|
|
179
130
|
* context (`zerocool.task`); ZEROCOOL_TASK forces it for a hand-run sandbox.
|
|
@@ -190,49 +141,43 @@ export function dispatchTaskKind(ctx, env = {}) {
|
|
|
190
141
|
return "opencode";
|
|
191
142
|
}
|
|
192
143
|
/**
|
|
193
|
-
* Read one sandboxed dispatch
|
|
144
|
+
* Read one sandboxed dispatch from the environment.
|
|
145
|
+
*
|
|
146
|
+
* The launcher contract comes from `readSandboxDispatch`, which is the one
|
|
147
|
+
* reader of those names on the TypeScript side. It fails, never falls back: a
|
|
148
|
+
* launch without a grant, an endpoint or a task is a defect in the launch, not
|
|
149
|
+
* something to guess around by running opencode with no task or by making
|
|
150
|
+
* unauthenticated calls.
|
|
194
151
|
*
|
|
195
152
|
* Deliberately never reads `GIBSON_BOOTSTRAP_TOKEN` or a host key: a sandboxed
|
|
196
153
|
* run is authenticated by its per-dispatch grant alone, and reaching for a
|
|
197
154
|
* bootstrap token here would reintroduce the enrollment handshake this shape
|
|
198
155
|
* exists to avoid.
|
|
199
156
|
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
157
|
+
* What this adds on top are this host's own options — the workspace, the
|
|
158
|
+
* deadline, the opencode session to continue, and whether to dial the callback
|
|
159
|
+
* listener without TLS. The launcher writes none of them, so the SDK carries
|
|
160
|
+
* none of them, and reading them here is not a second spelling of the contract.
|
|
204
161
|
*/
|
|
205
162
|
export function readDispatchContext(env, opts = {}) {
|
|
206
|
-
const
|
|
207
|
-
const token = env.GIBSON_CG_JWT;
|
|
208
|
-
if (!endpoint) {
|
|
209
|
-
throw new Error("GIBSON_CALLBACK_ENDPOINT is not set. A sandboxed dispatched run is authenticated by " +
|
|
210
|
-
"the per-dispatch grant the sandbox launch injects, not by a bootstrap token — the " +
|
|
211
|
-
"launch must supply the callback endpoint and the grant.");
|
|
212
|
-
}
|
|
213
|
-
if (!token) {
|
|
214
|
-
throw new Error("GIBSON_CALLBACK_ENDPOINT is set but GIBSON_CG_JWT is not. The callbacks would have no " +
|
|
215
|
-
"grant to authenticate with; a sandboxed run must not fall back to any other identity, " +
|
|
216
|
-
"so this fails instead.");
|
|
217
|
-
}
|
|
218
|
-
const task = decodeTask(env.GIBSON_AGENT_TASK_B64);
|
|
163
|
+
const dispatch = readSandboxDispatch(env);
|
|
219
164
|
const timeoutRaw = env.GIBSON_TIMEOUT_MS ? Number(env.GIBSON_TIMEOUT_MS) : 0;
|
|
220
165
|
return {
|
|
221
|
-
|
|
222
|
-
|
|
166
|
+
...dispatch,
|
|
167
|
+
taskContext: taskContextStrings(dispatch.task.context),
|
|
223
168
|
insecure: env.GIBSON_CALLBACK_INSECURE === "1",
|
|
224
|
-
missionRunId: env.GIBSON_MISSION_RUN_ID ?? "",
|
|
225
|
-
agentRunId: env.GIBSON_AGENT_RUN_ID ?? "",
|
|
226
|
-
traceId: env.GIBSON_TRACE_ID ?? "",
|
|
227
|
-
goal: task.goal,
|
|
228
|
-
taskContext: task.context,
|
|
229
|
-
...(env.GIBSON_MISSION_ID ? { missionId: env.GIBSON_MISSION_ID } : {}),
|
|
230
169
|
workspace: env.ZEROCOOL_WORKSPACE ?? opts.cwd ?? process.cwd(),
|
|
231
|
-
...(env.GIBSON_MODEL ? { model: env.GIBSON_MODEL } : {}),
|
|
232
170
|
...(env.GIBSON_OPENCODE_SESSION_ID ? { sessionId: env.GIBSON_OPENCODE_SESSION_ID } : {}),
|
|
233
171
|
timeoutMs: Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : 0,
|
|
234
172
|
};
|
|
235
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* The task harness for this dispatch: the grant the launch injected, nothing
|
|
176
|
+
* else. `sandboxHarness` is the SDK's opener for exactly this shape.
|
|
177
|
+
*/
|
|
178
|
+
export function dispatchHarness(ctx) {
|
|
179
|
+
return sandboxHarness(ctx, { insecure: ctx.insecure });
|
|
180
|
+
}
|
|
236
181
|
/**
|
|
237
182
|
* Run one dispatched Task headless and map it to a terminal outcome.
|
|
238
183
|
*
|
|
@@ -259,7 +204,16 @@ export async function runDispatch(ctx, deps = {}) {
|
|
|
259
204
|
...(ctx.model ? { model: ctx.model } : {}),
|
|
260
205
|
...(ctx.sessionId ? { sessionId: ctx.sessionId } : {}),
|
|
261
206
|
...(ctx.timeoutMs > 0 ? { timeoutMs: ctx.timeoutMs } : {}),
|
|
262
|
-
|
|
207
|
+
// The child speaks the opencode plugin's own contract, whose token name is
|
|
208
|
+
// GIBSON_CALLBACK_TOKEN. It is fed from the grant the SDK read.
|
|
209
|
+
env: dispatchChildEnv({
|
|
210
|
+
callbackEndpoint: ctx.callbackEndpoint,
|
|
211
|
+
callbackToken: ctx.grant,
|
|
212
|
+
insecure: ctx.insecure,
|
|
213
|
+
missionRunId: ctx.missionRunId,
|
|
214
|
+
agentRunId: ctx.agentRunId,
|
|
215
|
+
traceId: ctx.traceId,
|
|
216
|
+
}),
|
|
263
217
|
...(deps.onEvent ? { onEvent: deps.onEvent } : {}),
|
|
264
218
|
});
|
|
265
219
|
return {
|
|
@@ -288,7 +242,7 @@ export async function runSourceAnalysisDispatch(ctx, deps = {}) {
|
|
|
288
242
|
const opened = !deps.harness && !(deps.model && deps.findings);
|
|
289
243
|
const harness = deps.model && deps.findings
|
|
290
244
|
? undefined
|
|
291
|
-
: (deps.harness ??
|
|
245
|
+
: (deps.harness ?? dispatchHarness)(ctx);
|
|
292
246
|
const model = deps.model ?? harnessTriageModel(harness);
|
|
293
247
|
const findings = deps.findings ?? taskFindingsBackend(harness);
|
|
294
248
|
const sub = ctx.taskContext["source.path"];
|
|
@@ -385,10 +339,7 @@ export async function runWatchDispatch(ctx, deps = {}) {
|
|
|
385
339
|
// A harness is opened unless every seam that would use one was injected.
|
|
386
340
|
const needsHarness = !w.gitlab || !w.checkpoints || !w.scans || !w.credential;
|
|
387
341
|
const opened = needsHarness && !deps.harness;
|
|
388
|
-
const harness = needsHarness
|
|
389
|
-
? (deps.harness ??
|
|
390
|
-
((c) => openTaskHarness({ endpoint: c.callbackEndpoint, token: c.callbackToken, insecure: c.insecure ?? false })))(ctx)
|
|
391
|
-
: undefined;
|
|
342
|
+
const harness = needsHarness ? (deps.harness ?? dispatchHarness)(ctx) : undefined;
|
|
392
343
|
try {
|
|
393
344
|
const gitlab = w.gitlab ??
|
|
394
345
|
gitlabRest({
|
|
@@ -493,10 +444,7 @@ export async function runFixDispatch(ctx, deps = {}) {
|
|
|
493
444
|
}
|
|
494
445
|
const needsHarness = !f.gitlab || !f.findings || !f.status || !f.credential;
|
|
495
446
|
const opened = needsHarness && !deps.harness;
|
|
496
|
-
const harness = needsHarness
|
|
497
|
-
? (deps.harness ??
|
|
498
|
-
((c) => openTaskHarness({ endpoint: c.callbackEndpoint, token: c.callbackToken, insecure: c.insecure ?? false })))(ctx)
|
|
499
|
-
: undefined;
|
|
447
|
+
const harness = needsHarness ? (deps.harness ?? dispatchHarness)(ctx) : undefined;
|
|
500
448
|
try {
|
|
501
449
|
const gitlab = f.gitlab ??
|
|
502
450
|
gitlabRestWriter({
|
package/dist/findings.d.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
-
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
1
|
import { type Finding, type GibsonSession, type TaskHarness } from "@zeroroot-ai/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Findings — emit what the agent discovers into the tenant World
|
|
4
|
+
* (zerocool-plugins#7).
|
|
5
|
+
*
|
|
6
|
+
* One code path, two backends. With a Gibson session a finding goes to the
|
|
7
|
+
* tenant graph through `ComponentService.SubmitFinding` under the agent's
|
|
8
|
+
* COMPONENT identity; standalone it is appended to a local JSONL log. The tool
|
|
9
|
+
* the model sees is identical either way, so a prompt that works standalone
|
|
10
|
+
* works on the platform.
|
|
11
|
+
*
|
|
12
|
+
* The agent decides what is a finding. Nothing here invents findings from file
|
|
13
|
+
* edits or message traffic: a `file.edited` event is not a security finding, and
|
|
14
|
+
* auto-emitting one would fill the tenant graph with noise that a human then has
|
|
15
|
+
* to triage.
|
|
16
|
+
*
|
|
17
|
+
* The `submit_finding` tool the model calls lives in the Gibson MCP server now
|
|
18
|
+
* (ADR-0008). What is left here is the backend the dispatched task kinds use
|
|
19
|
+
* when they submit a finding themselves, with no model in the loop:
|
|
20
|
+
* `source-analysis` triages semgrep candidates, and `dispatch` reports them
|
|
21
|
+
* under the task grant.
|
|
22
|
+
*/
|
|
3
23
|
export interface FindingsBackend {
|
|
4
24
|
/** Submit a finding; returns the identifier it was recorded under. */
|
|
5
25
|
submit(f: Finding): Promise<string>;
|
|
@@ -32,4 +52,3 @@ export interface SessionContext {
|
|
|
32
52
|
* `agent_name` is fixed to "zerocool" rather than exposed as an argument — a
|
|
33
53
|
* model must not be able to attribute its findings to another agent.
|
|
34
54
|
*/
|
|
35
|
-
export declare function submitFindingTool(backend: FindingsBackend, context: SessionContext): ToolDefinition;
|
package/dist/findings.js
CHANGED
|
@@ -1,24 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// SPDX-License-Identifier: Elastic-2.0
|
|
2
|
+
// Copyright 2026 Zero Root AI
|
|
3
|
+
import { submitFinding, } from "@zeroroot-ai/sdk";
|
|
3
4
|
import { appendFile, mkdir } from "node:fs/promises";
|
|
4
5
|
import { dirname } from "node:path";
|
|
5
|
-
/**
|
|
6
|
-
* Findings — emit what the agent discovers into the tenant World
|
|
7
|
-
* (zerocool-plugins#7).
|
|
8
|
-
*
|
|
9
|
-
* One code path, two backends. With a Gibson session a finding goes to the
|
|
10
|
-
* tenant graph through `ComponentService.SubmitFinding` under the agent's
|
|
11
|
-
* COMPONENT identity; standalone it is appended to a local JSONL log. The tool
|
|
12
|
-
* the model sees is identical either way, so a prompt that works standalone
|
|
13
|
-
* works on the platform.
|
|
14
|
-
*
|
|
15
|
-
* The agent decides what is a finding. Nothing here invents findings from file
|
|
16
|
-
* edits or message traffic: a `file.edited` event is not a security finding, and
|
|
17
|
-
* auto-emitting one would fill the tenant graph with noise that a human then has
|
|
18
|
-
* to triage. The `event` hook only tracks session context used to stamp findings
|
|
19
|
-
* with provenance.
|
|
20
|
-
*/
|
|
21
|
-
const SEVERITIES = ["critical", "high", "medium", "low", "info"];
|
|
22
6
|
/** Platform backend — findings land in the tenant knowledge graph. */
|
|
23
7
|
export function gibsonFindingsBackend(session) {
|
|
24
8
|
return {
|
|
@@ -82,64 +66,3 @@ export function localFindingsBackend(path) {
|
|
|
82
66
|
* `agent_name` is fixed to "zerocool" rather than exposed as an argument — a
|
|
83
67
|
* model must not be able to attribute its findings to another agent.
|
|
84
68
|
*/
|
|
85
|
-
export function submitFindingTool(backend, context) {
|
|
86
|
-
const z = tool.schema;
|
|
87
|
-
return tool({
|
|
88
|
-
description: "Record a security finding in the Gibson knowledge graph. Use this when you " +
|
|
89
|
-
"discover a real vulnerability, misconfiguration or notable security fact about " +
|
|
90
|
-
"the target or codebase — not for progress updates or general observations.",
|
|
91
|
-
args: {
|
|
92
|
-
title: z.string().describe("One-line summary of the finding."),
|
|
93
|
-
description: z.string().describe("What the issue is, where it is, and why it matters."),
|
|
94
|
-
category: z
|
|
95
|
-
.string()
|
|
96
|
-
.describe('Type of issue, e.g. "injection", "auth", "secrets-exposure", "misconfiguration".'),
|
|
97
|
-
severity: z.enum(SEVERITIES).describe("Impact level of the finding."),
|
|
98
|
-
confidence: z
|
|
99
|
-
.number()
|
|
100
|
-
.min(0)
|
|
101
|
-
.max(1)
|
|
102
|
-
.optional()
|
|
103
|
-
.describe("How certain you are, from 0 to 1. Defaults to 1."),
|
|
104
|
-
evidence: z
|
|
105
|
-
.string()
|
|
106
|
-
.optional()
|
|
107
|
-
.describe("Supporting evidence — a code excerpt, request/response, or log line."),
|
|
108
|
-
remediation: z.string().optional().describe("How to fix or mitigate the issue."),
|
|
109
|
-
target_id: z.string().optional().describe("Identifier of the affected target or component."),
|
|
110
|
-
tags: z.array(z.string()).optional().describe("Labels for filtering."),
|
|
111
|
-
},
|
|
112
|
-
async execute(args, ctx) {
|
|
113
|
-
const finding = newFinding({
|
|
114
|
-
title: args.title,
|
|
115
|
-
description: args.description,
|
|
116
|
-
category: args.category,
|
|
117
|
-
severity: args.severity,
|
|
118
|
-
confidence: args.confidence,
|
|
119
|
-
missionID: context.sessionID ?? ctx.sessionID,
|
|
120
|
-
agentName: "zerocool",
|
|
121
|
-
...(args.remediation ? { remediation: args.remediation } : {}),
|
|
122
|
-
...(args.target_id ? { targetID: args.target_id } : {}),
|
|
123
|
-
...(args.tags ? { tags: args.tags } : {}),
|
|
124
|
-
...(args.evidence
|
|
125
|
-
? {
|
|
126
|
-
evidence: [
|
|
127
|
-
{
|
|
128
|
-
type: "text",
|
|
129
|
-
title: "evidence",
|
|
130
|
-
content: args.evidence,
|
|
131
|
-
timestamp: new Date().toISOString(),
|
|
132
|
-
},
|
|
133
|
-
],
|
|
134
|
-
}
|
|
135
|
-
: {}),
|
|
136
|
-
});
|
|
137
|
-
const id = await backend.submit(finding);
|
|
138
|
-
return {
|
|
139
|
-
title: `${args.severity}: ${args.title}`,
|
|
140
|
-
output: `Recorded finding ${id} in ${backend.describe()}.`,
|
|
141
|
-
metadata: { finding_id: id, severity: args.severity, category: args.category },
|
|
142
|
-
};
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
}
|
package/dist/fix.js
CHANGED