@zeroroot-ai/zerocool 0.9.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.js +2 -0
- package/dist/fix.js +2 -0
- package/dist/gitlab.js +2 -0
- package/dist/http-probe.js +2 -0
- package/dist/index.js +6 -1
- package/dist/knowledge-source.d.ts +34 -9
- package/dist/knowledge-source.js +33 -12
- package/dist/knowledge.js +2 -0
- package/dist/mcp-config.js +2 -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 +4 -4
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.js
CHANGED
package/dist/fix.js
CHANGED
package/dist/gitlab.js
CHANGED
package/dist/http-probe.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Elastic-2.0
|
|
2
|
+
// Copyright 2026 Zero Root AI
|
|
1
3
|
import { decodeToolInput } from "@zeroroot-ai/sdk";
|
|
2
4
|
export const HTTP_PROBE_TIMEOUT_MS = 15_000;
|
|
3
5
|
/** Perform the probe. Exported separately from the handler so it is testable without a work item. */
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Elastic-2.0
|
|
2
|
+
// Copyright 2026 Zero Root AI
|
|
1
3
|
import { connectGibson, startCompletionsShim } from "@zeroroot-ai/sdk";
|
|
2
4
|
import { existsSync } from "node:fs";
|
|
3
5
|
import { homedir } from "node:os";
|
|
@@ -111,7 +113,7 @@ export const GibsonPlugin = async () => {
|
|
|
111
113
|
// Which grant this process reads the knowledge graph with. A dispatched run
|
|
112
114
|
// reads as the TASK; an interactive one keeps the component grant. Chosen once
|
|
113
115
|
// here so nothing downstream has to ask.
|
|
114
|
-
const { knowledge, scope: knowledgeScope } = selectKnowledgeSource(live);
|
|
116
|
+
const { knowledge, scope: knowledgeScope, stop: stopKnowledge } = selectKnowledgeSource(live);
|
|
115
117
|
console.error(`[zerocool] knowledge reads use the ${knowledgeScope} grant`);
|
|
116
118
|
const injectKnowledge = ambientKnowledge(knowledge, process.env.ZEROCOOL_AMBIENT_QUERY ?? "prior findings and security facts for this codebase");
|
|
117
119
|
return {
|
|
@@ -147,6 +149,9 @@ export const GibsonPlugin = async () => {
|
|
|
147
149
|
context.sessionID = sessionID;
|
|
148
150
|
},
|
|
149
151
|
dispose: async () => {
|
|
152
|
+
// A task-scoped source holds a grant-renewal timer. Leaving it running
|
|
153
|
+
// would keep renewing a grant for a session that has ended.
|
|
154
|
+
stopKnowledge();
|
|
150
155
|
session?.stop();
|
|
151
156
|
await shim?.close();
|
|
152
157
|
},
|
|
@@ -1,12 +1,35 @@
|
|
|
1
|
-
import { type GibsonSession, type KnowledgeSource } from "@zeroroot-ai/sdk";
|
|
1
|
+
import { type GibsonSession, type KnowledgeSource, type OpenTaskHarnessOptions, type TaskHarness } from "@zeroroot-ai/sdk";
|
|
2
|
+
/** The chosen knowledge source, and the way to release what it opened. */
|
|
3
|
+
export interface SelectedKnowledge {
|
|
4
|
+
knowledge: KnowledgeSource;
|
|
5
|
+
scope: "task" | "component";
|
|
6
|
+
/**
|
|
7
|
+
* Release the task harness. Safe to call on either scope, and safe to call
|
|
8
|
+
* twice. A component-scoped run opens nothing, so this does nothing.
|
|
9
|
+
*/
|
|
10
|
+
stop: () => void;
|
|
11
|
+
}
|
|
12
|
+
/** Seams, so a test can read what a dispatched run actually sends. */
|
|
13
|
+
export interface KnowledgeSourceDeps {
|
|
14
|
+
/** Opens the task harness. Defaults to `openTaskHarness` on the dispatch grant. */
|
|
15
|
+
openHarness?: (opts: OpenTaskHarnessOptions) => TaskHarness;
|
|
16
|
+
}
|
|
2
17
|
/**
|
|
3
18
|
* Pick the grant this process should read the knowledge graph with.
|
|
4
19
|
*
|
|
5
20
|
* A DISPATCHED run must read as the TASK, not as the component. gibson mints a
|
|
6
|
-
* per-dispatch capability grant and
|
|
7
|
-
* GIBSON_CALLBACK_ENDPOINT / GIBSON_CALLBACK_TOKEN. Reading with the
|
|
8
|
-
* own grant instead is broader authority than the dispatch
|
|
9
|
-
* no per-task attribution on anything the run reads — the
|
|
21
|
+
* per-dispatch capability grant, and the driver passes it to this opencode
|
|
22
|
+
* child as GIBSON_CALLBACK_ENDPOINT / GIBSON_CALLBACK_TOKEN. Reading with the
|
|
23
|
+
* component's own grant instead is broader authority than the dispatch
|
|
24
|
+
* intended and leaves no per-task attribution on anything the run reads — the
|
|
25
|
+
* gap ADR-0006 recorded.
|
|
26
|
+
*
|
|
27
|
+
* THESE TWO NAMES ARE THE CHILD CONTRACT, NOT THE LAUNCHER CONTRACT. The
|
|
28
|
+
* launcher writes GIBSON_CG_JWT and GIBSON_AGENT_TASK_B64 into the sandbox,
|
|
29
|
+
* and `readSandboxDispatch` in `@zeroroot-ai/sdk` is the one reader of those
|
|
30
|
+
* (see `dispatch.ts`). This module runs one process further down, inside
|
|
31
|
+
* opencode, whose environment `dispatchChildEnv` writes. It never sees the
|
|
32
|
+
* launch environment, so it reads the child names and nothing else.
|
|
10
33
|
*
|
|
11
34
|
* An INTERACTIVE run has no callback seam and keeps the component grant, which
|
|
12
35
|
* is correct: a human started it, and there is no task to scope to.
|
|
@@ -14,8 +37,10 @@ import { type GibsonSession, type KnowledgeSource } from "@zeroroot-ai/sdk";
|
|
|
14
37
|
* A dispatch that carries an endpoint but no token FAILS rather than falling
|
|
15
38
|
* back. Falling back is how the gap reappears silently, and a caller who wanted
|
|
16
39
|
* task-scoped reads would get component-scoped ones without being told.
|
|
40
|
+
*
|
|
41
|
+
* `openTaskHarness` derives the `ContextInfo` every callback RPC carries from
|
|
42
|
+
* the grant's own claims, so a task-scoped read reaches the daemon naming the
|
|
43
|
+
* mission it belongs to. It also renews the grant on a timer, which is why the
|
|
44
|
+
* caller must {@link SelectedKnowledge.stop} the source when the session ends.
|
|
17
45
|
*/
|
|
18
|
-
export declare function selectKnowledgeSource(session: GibsonSession):
|
|
19
|
-
knowledge: KnowledgeSource;
|
|
20
|
-
scope: "task" | "component";
|
|
21
|
-
};
|
|
46
|
+
export declare function selectKnowledgeSource(session: GibsonSession, deps?: KnowledgeSourceDeps): SelectedKnowledge;
|
package/dist/knowledge-source.js
CHANGED
|
@@ -1,12 +1,22 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Elastic-2.0
|
|
2
|
+
// Copyright 2026 Zero Root AI
|
|
1
3
|
import { componentKnowledge, openTaskHarness, taskKnowledge, } from "@zeroroot-ai/sdk";
|
|
2
4
|
/**
|
|
3
5
|
* Pick the grant this process should read the knowledge graph with.
|
|
4
6
|
*
|
|
5
7
|
* A DISPATCHED run must read as the TASK, not as the component. gibson mints a
|
|
6
|
-
* per-dispatch capability grant and
|
|
7
|
-
* GIBSON_CALLBACK_ENDPOINT / GIBSON_CALLBACK_TOKEN. Reading with the
|
|
8
|
-
* own grant instead is broader authority than the dispatch
|
|
9
|
-
* no per-task attribution on anything the run reads — the
|
|
8
|
+
* per-dispatch capability grant, and the driver passes it to this opencode
|
|
9
|
+
* child as GIBSON_CALLBACK_ENDPOINT / GIBSON_CALLBACK_TOKEN. Reading with the
|
|
10
|
+
* component's own grant instead is broader authority than the dispatch
|
|
11
|
+
* intended and leaves no per-task attribution on anything the run reads — the
|
|
12
|
+
* gap ADR-0006 recorded.
|
|
13
|
+
*
|
|
14
|
+
* THESE TWO NAMES ARE THE CHILD CONTRACT, NOT THE LAUNCHER CONTRACT. The
|
|
15
|
+
* launcher writes GIBSON_CG_JWT and GIBSON_AGENT_TASK_B64 into the sandbox,
|
|
16
|
+
* and `readSandboxDispatch` in `@zeroroot-ai/sdk` is the one reader of those
|
|
17
|
+
* (see `dispatch.ts`). This module runs one process further down, inside
|
|
18
|
+
* opencode, whose environment `dispatchChildEnv` writes. It never sees the
|
|
19
|
+
* launch environment, so it reads the child names and nothing else.
|
|
10
20
|
*
|
|
11
21
|
* An INTERACTIVE run has no callback seam and keeps the component grant, which
|
|
12
22
|
* is correct: a human started it, and there is no task to scope to.
|
|
@@ -14,25 +24,36 @@ import { componentKnowledge, openTaskHarness, taskKnowledge, } from "@zeroroot-a
|
|
|
14
24
|
* A dispatch that carries an endpoint but no token FAILS rather than falling
|
|
15
25
|
* back. Falling back is how the gap reappears silently, and a caller who wanted
|
|
16
26
|
* task-scoped reads would get component-scoped ones without being told.
|
|
27
|
+
*
|
|
28
|
+
* `openTaskHarness` derives the `ContextInfo` every callback RPC carries from
|
|
29
|
+
* the grant's own claims, so a task-scoped read reaches the daemon naming the
|
|
30
|
+
* mission it belongs to. It also renews the grant on a timer, which is why the
|
|
31
|
+
* caller must {@link SelectedKnowledge.stop} the source when the session ends.
|
|
17
32
|
*/
|
|
18
|
-
export function selectKnowledgeSource(session) {
|
|
33
|
+
export function selectKnowledgeSource(session, deps = {}) {
|
|
19
34
|
const endpoint = process.env.GIBSON_CALLBACK_ENDPOINT;
|
|
20
35
|
const token = process.env.GIBSON_CALLBACK_TOKEN;
|
|
21
36
|
if (!endpoint) {
|
|
22
|
-
return {
|
|
37
|
+
return {
|
|
38
|
+
knowledge: componentKnowledge(session.clients.component),
|
|
39
|
+
scope: "component",
|
|
40
|
+
stop: () => { },
|
|
41
|
+
};
|
|
23
42
|
}
|
|
24
43
|
if (!token) {
|
|
25
44
|
throw new Error("GIBSON_CALLBACK_ENDPOINT is set but GIBSON_CALLBACK_TOKEN is not. A dispatched run " +
|
|
26
45
|
"must read with its task grant; falling back to the component grant would silently " +
|
|
27
46
|
"widen this run's authority.");
|
|
28
47
|
}
|
|
48
|
+
const harness = (deps.openHarness ?? openTaskHarness)({
|
|
49
|
+
endpoint,
|
|
50
|
+
token,
|
|
51
|
+
// A local or kind daemon may serve the callback listener without TLS.
|
|
52
|
+
insecure: process.env.GIBSON_CALLBACK_INSECURE === "1",
|
|
53
|
+
});
|
|
29
54
|
return {
|
|
30
|
-
knowledge: taskKnowledge(
|
|
31
|
-
endpoint,
|
|
32
|
-
token,
|
|
33
|
-
// A local or kind daemon may serve the callback listener without TLS.
|
|
34
|
-
insecure: process.env.GIBSON_CALLBACK_INSECURE === "1",
|
|
35
|
-
})),
|
|
55
|
+
knowledge: taskKnowledge(harness),
|
|
36
56
|
scope: "task",
|
|
57
|
+
stop: () => harness.stop(),
|
|
37
58
|
};
|
|
38
59
|
}
|
package/dist/knowledge.js
CHANGED
package/dist/mcp-config.js
CHANGED
package/dist/opencode-run.js
CHANGED
package/dist/semgrep.js
CHANGED
package/dist/serve-agent.js
CHANGED
package/dist/serve.js
CHANGED
package/dist/source-analysis.js
CHANGED
package/dist/watch.js
CHANGED
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeroroot-ai/zerocool",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "opencode host adapter for Gibson (zerocool): zero-config LLM through the harness, ambient knowledge, and the Gibson MCP server registered for you. The tools come from @zeroroot-ai/gibson-mcp.",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Elastic-2.0",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/zeroroot-ai/zerocool-plugins.git",
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"test": "tsc -p tsconfig.test.json && node --test \"dist-test/**/*.test.js\""
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@zeroroot-ai/sdk": "^0.
|
|
26
|
+
"@zeroroot-ai/sdk": "^0.12.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"@opencode-ai/plugin": "*"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
|
-
"@opencode-ai/plugin": "^1.18.
|
|
32
|
+
"@opencode-ai/plugin": "^1.18.27"
|
|
33
33
|
},
|
|
34
34
|
"bin": {
|
|
35
35
|
"zerocool-serve": "dist/serve.js",
|