@zeroroot-ai/zerocool 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/dispatch.d.ts +2 -1
- package/dist/dispatch.js +22 -6
- package/dist/watch.d.ts +47 -33
- package/dist/watch.js +59 -69
- package/package.json +2 -2
package/dist/dispatch.d.ts
CHANGED
|
@@ -262,7 +262,8 @@ export declare function runSourceAnalysisDispatch(ctx: DispatchContext, deps?: D
|
|
|
262
262
|
* `gitlab.url` instance base url; defaults to gitlab.com
|
|
263
263
|
* `gitlab.ref` branch to watch; defaults to `main`
|
|
264
264
|
* `gitlab.credential` tenant secret holding the project access token
|
|
265
|
-
* `repository.url` clone url
|
|
265
|
+
* `repository.url` clone url the Scan mission checks out (required)
|
|
266
|
+
* `image.ref` the image the pipeline published, by digest (required)
|
|
266
267
|
* `target.id` the registered target each Scan mission binds to
|
|
267
268
|
* `watch.interval_ms` poll cadence; defaults to 30s
|
|
268
269
|
*/
|
package/dist/dispatch.js
CHANGED
|
@@ -342,7 +342,8 @@ export async function runSourceAnalysisDispatch(ctx, deps = {}) {
|
|
|
342
342
|
* `gitlab.url` instance base url; defaults to gitlab.com
|
|
343
343
|
* `gitlab.ref` branch to watch; defaults to `main`
|
|
344
344
|
* `gitlab.credential` tenant secret holding the project access token
|
|
345
|
-
* `repository.url` clone url
|
|
345
|
+
* `repository.url` clone url the Scan mission checks out (required)
|
|
346
|
+
* `image.ref` the image the pipeline published, by digest (required)
|
|
346
347
|
* `target.id` the registered target each Scan mission binds to
|
|
347
348
|
* `watch.interval_ms` poll cadence; defaults to 30s
|
|
348
349
|
*/
|
|
@@ -353,6 +354,8 @@ export async function runWatchDispatch(ctx, deps = {}) {
|
|
|
353
354
|
const ref = ctx.taskContext["gitlab.ref"] || DEFAULT_REF;
|
|
354
355
|
const targetId = ctx.taskContext["target.id"] || "";
|
|
355
356
|
const credentialName = ctx.taskContext["gitlab.credential"] || "gitlab-token";
|
|
357
|
+
const repositoryUrl = ctx.taskContext["repository.url"] || "";
|
|
358
|
+
const imageRef = ctx.taskContext["image.ref"] || "";
|
|
356
359
|
if (!application) {
|
|
357
360
|
throw new Error("watch: the mission node set no `application` in the task context. The watch keys its " +
|
|
358
361
|
"checkpoints and its Scan missions by Application; without one it cannot tell its own " +
|
|
@@ -362,6 +365,23 @@ export async function runWatchDispatch(ctx, deps = {}) {
|
|
|
362
365
|
throw new Error("watch: the mission node set no `gitlab.project` in the task context, so there is no " +
|
|
363
366
|
"project to poll.");
|
|
364
367
|
}
|
|
368
|
+
// Checked here, not at the first trigger. The checked-in Scan mission requires
|
|
369
|
+
// all seven of its parameters — CUE refuses an incomplete render rather than
|
|
370
|
+
// substituting an empty string — so a watch missing one is already broken. It
|
|
371
|
+
// would otherwise poll cleanly for hours and fail on the first pipeline, at
|
|
372
|
+
// which point the failure looks like the pipeline's fault rather than the
|
|
373
|
+
// watch's configuration.
|
|
374
|
+
if (!w.scans) {
|
|
375
|
+
const missing = [
|
|
376
|
+
...(repositoryUrl ? [] : ["repository.url"]),
|
|
377
|
+
...(imageRef ? [] : ["image.ref"]),
|
|
378
|
+
];
|
|
379
|
+
if (missing.length > 0) {
|
|
380
|
+
throw new Error(`watch: the mission node set no ${missing.join(" and no ")} in the task context. The ` +
|
|
381
|
+
"checked-in Scan mission requires every one of its seven parameters, so a Scan " +
|
|
382
|
+
"originated without these would be refused rather than run.");
|
|
383
|
+
}
|
|
384
|
+
}
|
|
365
385
|
// A harness is opened unless every seam that would use one was injected.
|
|
366
386
|
const needsHarness = !w.gitlab || !w.checkpoints || !w.scans || !w.credential;
|
|
367
387
|
const opened = needsHarness && !deps.harness;
|
|
@@ -380,11 +400,7 @@ export async function runWatchDispatch(ctx, deps = {}) {
|
|
|
380
400
|
const scans = w.scans ??
|
|
381
401
|
harnessScanLauncher(harness, {
|
|
382
402
|
targetId,
|
|
383
|
-
inputs: {
|
|
384
|
-
application,
|
|
385
|
-
repositoryUrl: ctx.taskContext["repository.url"] || "",
|
|
386
|
-
...(ctx.taskContext["image.ref"] ? { imageRef: ctx.taskContext["image.ref"] } : {}),
|
|
387
|
-
},
|
|
403
|
+
inputs: { application, repositoryUrl, imageRef },
|
|
388
404
|
});
|
|
389
405
|
const intervalMs = Number(ctx.taskContext["watch.interval_ms"] || Number.NaN);
|
|
390
406
|
const summary = await runWatchLoop({
|
package/dist/watch.d.ts
CHANGED
|
@@ -156,48 +156,40 @@ export declare function parseCheckpoint(text: string): {
|
|
|
156
156
|
export declare function worldCheckpoints(harness: TaskHarness, application: string): WatchCheckpoints;
|
|
157
157
|
/** The recall text for an application's checkpoints. Exported so a test names it once. */
|
|
158
158
|
export declare function checkpointQueryText(application: string): string;
|
|
159
|
+
/**
|
|
160
|
+
* The name of the checked-in Scan mission in gibson's mission catalog.
|
|
161
|
+
*
|
|
162
|
+
* One file in `internal/platform/missioncatalog/missions/` is one mission,
|
|
163
|
+
* named for its file: `scan.cue` is `"scan"`.
|
|
164
|
+
*/
|
|
165
|
+
export declare const SCAN_CATALOG_MISSION = "scan";
|
|
159
166
|
/** What a Scan mission needs to know about the pipeline that triggered it. */
|
|
160
167
|
export interface ScanMissionInputs {
|
|
161
168
|
application: string;
|
|
162
169
|
pipeline: Pipeline;
|
|
163
170
|
/** The project's clone url, so the Scan mission knows what to check out. */
|
|
164
171
|
repositoryUrl: string;
|
|
165
|
-
/** The image the pipeline
|
|
166
|
-
imageRef
|
|
167
|
-
/** The registered target the runtime scans are allowed to reach. */
|
|
168
|
-
targetId?: string;
|
|
172
|
+
/** The image the pipeline published, by digest. */
|
|
173
|
+
imageRef: string;
|
|
169
174
|
}
|
|
170
175
|
/**
|
|
171
|
-
* The Scan mission
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
* in the node's task context, and the swap is this function's body alone.
|
|
185
|
-
*
|
|
186
|
-
* The definition is built as protojson by hand rather than through the
|
|
187
|
-
* generated schema because the SDK's package exports only its index, and the
|
|
188
|
-
* mission schema is not re-exported from it.
|
|
176
|
+
* The seven parameters the checked-in Scan mission is rendered with.
|
|
177
|
+
*
|
|
178
|
+
* These names are the catalog's, not ours: `missioncatalog.Params.fields()` is
|
|
179
|
+
* the one declaration of them, and the daemon REFUSES an unrecognised key
|
|
180
|
+
* rather than ignoring it. That refusal is the smuggling defence — `Params` has
|
|
181
|
+
* no target or host field, so the runtime target binds from the mission's
|
|
182
|
+
* `target_id` alone and a caller cannot point a scan at a host the tenant never
|
|
183
|
+
* registered. A key renamed here without renaming it there fails loudly at the
|
|
184
|
+
* next launch, which is the intended outcome.
|
|
185
|
+
*
|
|
186
|
+
* Every one is required. CUE refuses an incomplete render rather than
|
|
187
|
+
* substituting an empty string, so a missing commit fails instead of scanning
|
|
188
|
+
* HEAD and a missing image fails instead of scanning nothing.
|
|
189
189
|
*/
|
|
190
|
-
export declare function
|
|
190
|
+
export declare function scanCatalogParams(inputs: ScanMissionInputs): Record<string, string>;
|
|
191
191
|
/** The child mission's name, which is also how a person finds it in the dashboard. */
|
|
192
192
|
export declare function scanMissionName(inputs: ScanMissionInputs): string;
|
|
193
|
-
/**
|
|
194
|
-
* Render a string map as `map<string, gibson.common.v1.TypedValue>` in
|
|
195
|
-
* protojson. Every value the loop passes is a string, so every entry is a
|
|
196
|
-
* `stringValue`.
|
|
197
|
-
*/
|
|
198
|
-
export declare function typedContext(ctx: Record<string, string>): Record<string, {
|
|
199
|
-
stringValue: string;
|
|
200
|
-
}>;
|
|
201
193
|
export interface HarnessScanLauncherOptions {
|
|
202
194
|
/** Everything but the pipeline, which the launcher fills in per trigger. */
|
|
203
195
|
inputs: Omit<ScanMissionInputs, "pipeline">;
|
|
@@ -205,8 +197,6 @@ export interface HarnessScanLauncherOptions {
|
|
|
205
197
|
targetId: string;
|
|
206
198
|
/** Ceiling on one Scan. Defaults to {@link DEFAULT_SCAN_TIMEOUT_MS}. */
|
|
207
199
|
scanTimeoutMs?: number;
|
|
208
|
-
/** Override the definition, so gibson#1660 can supply the real one. */
|
|
209
|
-
definition?: (inputs: ScanMissionInputs) => unknown;
|
|
210
200
|
}
|
|
211
201
|
/**
|
|
212
202
|
* A {@link ScanLauncher} over the harness callbacks, on the dispatch grant.
|
|
@@ -215,5 +205,29 @@ export interface HarnessScanLauncherOptions {
|
|
|
215
205
|
* daemon resolves the parent from this harness, so the Scan mission is a child
|
|
216
206
|
* of the watch mission and inherits its tenant. Nothing in the request names a
|
|
217
207
|
* parent, a tenant or an originator, so nothing here can widen them.
|
|
208
|
+
*
|
|
209
|
+
* THE MISSION IS NAMED, NOT BUILT (ADR-0018, gibson#1688). The Scan mission is
|
|
210
|
+
* checked into gibson's `missioncatalog` and that copy is authoritative; this
|
|
211
|
+
* launcher sends its name and seven parameters. It used to build its own graph,
|
|
212
|
+
* because `CreateMission` had no way to ask for the catalog's copy — two
|
|
213
|
+
* descriptions of one mission, which is the parallel definition ADR-0027
|
|
214
|
+
* forbids. There is no fallback to the old path: a definition that only one of
|
|
215
|
+
* the two knew about is the drift the single copy exists to prevent.
|
|
216
|
+
*
|
|
217
|
+
* The request goes through the SDK's `buildCreateMissionRequest` rather than
|
|
218
|
+
* being assembled here, because that builder normalises the one trap in this
|
|
219
|
+
* call. `JSON.stringify(undefined)` encodes to zero bytes but
|
|
220
|
+
* `JSON.stringify(null)` encodes to the four-byte string `"null"`, so a graph
|
|
221
|
+
* field filled in unconditionally puts a non-empty body on the wire beside the
|
|
222
|
+
* catalog name; the daemon refuses the pair, and the failure reads as "the
|
|
223
|
+
* catalog path does not work" rather than "you passed a null definition".
|
|
224
|
+
*
|
|
225
|
+
* The response is then sent by hand rather than through `createTaskMission`,
|
|
226
|
+
* for one reason: `CreateMissionResponse` carries an in-band `error`, and the
|
|
227
|
+
* daemon puts its most useful refusals there — an unknown parameter key naming
|
|
228
|
+
* the key and listing the known set, or every missing parameter reported at
|
|
229
|
+
* once. `createTaskMission` reads only `mission`, so those would surface as
|
|
230
|
+
* "returned no mission" and the operator would lose the sentence that says what
|
|
231
|
+
* to fix.
|
|
218
232
|
*/
|
|
219
233
|
export declare function harnessScanLauncher(harness: TaskHarness, opts: HarnessScanLauncherOptions): ScanLauncher;
|
package/dist/watch.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { remember, taskKnowledge } from "@zeroroot-ai/sdk";
|
|
1
|
+
import { buildCreateMissionRequest, remember, taskKnowledge } from "@zeroroot-ai/sdk";
|
|
2
2
|
import { TRIGGER_STATUS } from "./gitlab.js";
|
|
3
3
|
/**
|
|
4
4
|
* The always-on watch loop (zerocool-plugins#88).
|
|
@@ -222,81 +222,47 @@ export function worldCheckpoints(harness, application) {
|
|
|
222
222
|
export function checkpointQueryText(application) {
|
|
223
223
|
return `${CHECKPOINT_KIND} application=${application}`;
|
|
224
224
|
}
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// The Scan mission
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
225
228
|
/**
|
|
226
|
-
* The Scan mission
|
|
229
|
+
* The name of the checked-in Scan mission in gibson's mission catalog.
|
|
227
230
|
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
* One file in `internal/platform/missioncatalog/missions/` is one mission,
|
|
232
|
+
* named for its file: `scan.cue` is `"scan"`.
|
|
233
|
+
*/
|
|
234
|
+
export const SCAN_CATALOG_MISSION = "scan";
|
|
235
|
+
/**
|
|
236
|
+
* The seven parameters the checked-in Scan mission is rendered with.
|
|
232
237
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
236
|
-
* no
|
|
237
|
-
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
238
|
+
* These names are the catalog's, not ours: `missioncatalog.Params.fields()` is
|
|
239
|
+
* the one declaration of them, and the daemon REFUSES an unrecognised key
|
|
240
|
+
* rather than ignoring it. That refusal is the smuggling defence — `Params` has
|
|
241
|
+
* no target or host field, so the runtime target binds from the mission's
|
|
242
|
+
* `target_id` alone and a caller cannot point a scan at a host the tenant never
|
|
243
|
+
* registered. A key renamed here without renaming it there fails loudly at the
|
|
244
|
+
* next launch, which is the intended outcome.
|
|
240
245
|
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
246
|
+
* Every one is required. CUE refuses an incomplete render rather than
|
|
247
|
+
* substituting an empty string, so a missing commit fails instead of scanning
|
|
248
|
+
* HEAD and a missing image fails instead of scanning nothing.
|
|
244
249
|
*/
|
|
245
|
-
export function
|
|
246
|
-
const {
|
|
247
|
-
const context = {
|
|
248
|
-
"zerocool.task": "source-analysis",
|
|
249
|
-
application,
|
|
250
|
-
"pipeline.id": String(pipeline.id),
|
|
251
|
-
"pipeline.url": pipeline.webUrl,
|
|
252
|
-
"repository.url": inputs.repositoryUrl,
|
|
253
|
-
"repository.ref": pipeline.ref,
|
|
254
|
-
"repository.commit": pipeline.sha,
|
|
255
|
-
...(inputs.imageRef ? { "image.ref": inputs.imageRef } : {}),
|
|
256
|
-
...(inputs.targetId ? { "target.id": inputs.targetId } : {}),
|
|
257
|
-
};
|
|
250
|
+
export function scanCatalogParams(inputs) {
|
|
251
|
+
const { pipeline } = inputs;
|
|
258
252
|
return {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
type: "NODE_TYPE_AGENT",
|
|
267
|
-
name: "scan",
|
|
268
|
-
description: "Source analysis of the commit the pipeline built.",
|
|
269
|
-
agentConfig: {
|
|
270
|
-
agentName: "zerocool",
|
|
271
|
-
task: {
|
|
272
|
-
id: "scan",
|
|
273
|
-
goal: `Scan the ${application} repository at commit ${pipeline.sha} and submit a Finding for ` +
|
|
274
|
-
"every real weakness you can support with evidence.",
|
|
275
|
-
context: typedContext(context),
|
|
276
|
-
},
|
|
277
|
-
},
|
|
278
|
-
},
|
|
279
|
-
},
|
|
280
|
-
entryPoints: ["scan"],
|
|
253
|
+
application: inputs.application,
|
|
254
|
+
repositoryUrl: inputs.repositoryUrl,
|
|
255
|
+
ref: pipeline.ref,
|
|
256
|
+
commit: pipeline.sha,
|
|
257
|
+
pipelineId: String(pipeline.id),
|
|
258
|
+
pipelineUrl: pipeline.webUrl,
|
|
259
|
+
imageRef: inputs.imageRef,
|
|
281
260
|
};
|
|
282
261
|
}
|
|
283
262
|
/** The child mission's name, which is also how a person finds it in the dashboard. */
|
|
284
263
|
export function scanMissionName(inputs) {
|
|
285
264
|
return `scan ${inputs.application} pipeline ${inputs.pipeline.id}`;
|
|
286
265
|
}
|
|
287
|
-
/**
|
|
288
|
-
* Render a string map as `map<string, gibson.common.v1.TypedValue>` in
|
|
289
|
-
* protojson. Every value the loop passes is a string, so every entry is a
|
|
290
|
-
* `stringValue`.
|
|
291
|
-
*/
|
|
292
|
-
export function typedContext(ctx) {
|
|
293
|
-
const out = {};
|
|
294
|
-
for (const [k, v] of Object.entries(ctx)) {
|
|
295
|
-
if (v !== "")
|
|
296
|
-
out[k] = { stringValue: v };
|
|
297
|
-
}
|
|
298
|
-
return out;
|
|
299
|
-
}
|
|
300
266
|
/**
|
|
301
267
|
* A {@link ScanLauncher} over the harness callbacks, on the dispatch grant.
|
|
302
268
|
*
|
|
@@ -304,19 +270,43 @@ export function typedContext(ctx) {
|
|
|
304
270
|
* daemon resolves the parent from this harness, so the Scan mission is a child
|
|
305
271
|
* of the watch mission and inherits its tenant. Nothing in the request names a
|
|
306
272
|
* parent, a tenant or an originator, so nothing here can widen them.
|
|
273
|
+
*
|
|
274
|
+
* THE MISSION IS NAMED, NOT BUILT (ADR-0018, gibson#1688). The Scan mission is
|
|
275
|
+
* checked into gibson's `missioncatalog` and that copy is authoritative; this
|
|
276
|
+
* launcher sends its name and seven parameters. It used to build its own graph,
|
|
277
|
+
* because `CreateMission` had no way to ask for the catalog's copy — two
|
|
278
|
+
* descriptions of one mission, which is the parallel definition ADR-0027
|
|
279
|
+
* forbids. There is no fallback to the old path: a definition that only one of
|
|
280
|
+
* the two knew about is the drift the single copy exists to prevent.
|
|
281
|
+
*
|
|
282
|
+
* The request goes through the SDK's `buildCreateMissionRequest` rather than
|
|
283
|
+
* being assembled here, because that builder normalises the one trap in this
|
|
284
|
+
* call. `JSON.stringify(undefined)` encodes to zero bytes but
|
|
285
|
+
* `JSON.stringify(null)` encodes to the four-byte string `"null"`, so a graph
|
|
286
|
+
* field filled in unconditionally puts a non-empty body on the wire beside the
|
|
287
|
+
* catalog name; the daemon refuses the pair, and the failure reads as "the
|
|
288
|
+
* catalog path does not work" rather than "you passed a null definition".
|
|
289
|
+
*
|
|
290
|
+
* The response is then sent by hand rather than through `createTaskMission`,
|
|
291
|
+
* for one reason: `CreateMissionResponse` carries an in-band `error`, and the
|
|
292
|
+
* daemon puts its most useful refusals there — an unknown parameter key naming
|
|
293
|
+
* the key and listing the known set, or every missing parameter reported at
|
|
294
|
+
* once. `createTaskMission` reads only `mission`, so those would surface as
|
|
295
|
+
* "returned no mission" and the operator would lose the sentence that says what
|
|
296
|
+
* to fix.
|
|
307
297
|
*/
|
|
308
298
|
export function harnessScanLauncher(harness, opts) {
|
|
309
|
-
const build = opts.definition ?? scanMissionDefinition;
|
|
310
299
|
const timeoutMs = BigInt(opts.scanTimeoutMs ?? DEFAULT_SCAN_TIMEOUT_MS);
|
|
311
300
|
return {
|
|
312
301
|
async launch(pipeline) {
|
|
313
|
-
const inputs = { ...opts.inputs, pipeline
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
302
|
+
const inputs = { ...opts.inputs, pipeline };
|
|
303
|
+
const wire = buildCreateMissionRequest({
|
|
304
|
+
catalogMission: SCAN_CATALOG_MISSION,
|
|
305
|
+
catalogParams: scanCatalogParams(inputs),
|
|
317
306
|
targetId: opts.targetId,
|
|
318
307
|
name: scanMissionName(inputs),
|
|
319
308
|
});
|
|
309
|
+
const created = await harness.client.createMission({ context: harness.context, ...wire });
|
|
320
310
|
if (created.error) {
|
|
321
311
|
throw new Error(`CreateMission refused: ${created.error.message}`);
|
|
322
312
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeroroot-ai/zerocool",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "opencode plugin: Gibson platform integration (zerocool). Zero-config LLM, tools, findings, knowledge, delegate/missions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -23,7 +23,7 @@
|
|
|
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.11.0"
|
|
27
27
|
},
|
|
28
28
|
"peerDependencies": {
|
|
29
29
|
"@opencode-ai/plugin": "*"
|