@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.
@@ -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 handed to each Scan mission
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 handed to each Scan mission
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 built, when the watch was told how to name it. */
166
- imageRef?: string;
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 definition, as canonical proto JSON.
172
- *
173
- * STILL BUILT HERE, AND IT SHOULD NOT BE. gibson#1685 landed the Scan mission
174
- * as a checked-in definition in gibson's `missioncatalog`, and ADR-0018 makes
175
- * that copy authoritative: this function is meant to become a mission name plus
176
- * seven parameters, not a second description of the same graph.
177
- *
178
- * It cannot yet. `missioncatalog.Render` has no production caller, and
179
- * `CreateMission` carries only `mission_definition_json` — there is no field
180
- * naming a checked-in mission and none carrying its parameters, so an agent has
181
- * no way to ask for the catalog's copy. That is gibson#1688. Until it lands the
182
- * only way to originate a Scan is to send a graph, so this one stays, with the
183
- * shape the catalog definition expects: everything the fan-out needs is carried
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 scanMissionDefinition(inputs: ScanMissionInputs): unknown;
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 definition, as canonical proto JSON.
229
+ * The name of the checked-in Scan mission in gibson's mission catalog.
227
230
  *
228
- * STILL BUILT HERE, AND IT SHOULD NOT BE. gibson#1685 landed the Scan mission
229
- * as a checked-in definition in gibson's `missioncatalog`, and ADR-0018 makes
230
- * that copy authoritative: this function is meant to become a mission name plus
231
- * seven parameters, not a second description of the same graph.
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
- * It cannot yet. `missioncatalog.Render` has no production caller, and
234
- * `CreateMission` carries only `mission_definition_json` — there is no field
235
- * naming a checked-in mission and none carrying its parameters, so an agent has
236
- * no way to ask for the catalog's copy. That is gibson#1688. Until it lands the
237
- * only way to originate a Scan is to send a graph, so this one stays, with the
238
- * shape the catalog definition expects: everything the fan-out needs is carried
239
- * in the node's task context, and the swap is this function's body alone.
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
- * The definition is built as protojson by hand rather than through the
242
- * generated schema because the SDK's package exports only its index, and the
243
- * mission schema is not re-exported from it.
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 scanMissionDefinition(inputs) {
246
- const { application, pipeline } = inputs;
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
- name: scanMissionName(inputs),
260
- description: `Scan ${application} at ${pipeline.sha.slice(0, 12)}, originated by the always-on agent ` +
261
- `for pipeline ${pipeline.id}.`,
262
- version: "1.0.0",
263
- nodes: {
264
- scan: {
265
- id: "scan",
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, targetId: opts.targetId };
314
- const created = await harness.client.createMission({
315
- context: harness.context,
316
- missionDefinitionJson: new TextEncoder().encode(JSON.stringify(build(inputs))),
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.7.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.10.0"
26
+ "@zeroroot-ai/sdk": "^0.11.0"
27
27
  },
28
28
  "peerDependencies": {
29
29
  "@opencode-ai/plugin": "*"