@zeroroot-ai/zerocool 0.7.0 → 0.9.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({
@@ -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,6 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { newFinding, submitFinding, } from "@zeroroot-ai/sdk";
1
+ import { submitFinding, } from "@zeroroot-ai/sdk";
3
2
  import { appendFile, mkdir } from "node:fs/promises";
4
3
  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
4
  /** Platform backend — findings land in the tenant knowledge graph. */
23
5
  export function gibsonFindingsBackend(session) {
24
6
  return {
@@ -82,64 +64,3 @@ export function localFindingsBackend(path) {
82
64
  * `agent_name` is fixed to "zerocool" rather than exposed as an argument — a
83
65
  * model must not be able to attribute its findings to another agent.
84
66
  */
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/index.d.ts CHANGED
@@ -20,11 +20,17 @@ import type { Plugin } from "@opencode-ai/plugin";
20
20
  * - checks in as a Gibson component (Capability Grant + RegisterComponent + heartbeat),
21
21
  * - starts a local OpenAI-compatible shim over the harness,
22
22
  * - auto-registers a zero-config `gibson` provider via the `config` hook (#6),
23
- * - emits findings into the tenant World (#7),
24
- * - reads the tenant knowledge graph (#8),
25
- * - exposes Gibson tools and plugins as opencode tools (#9),
26
- * - delegates to other agents and drives missions (#10),
27
- * - enrolls produced artifacts into the fleet (#11).
23
+ * - reads the tenant knowledge graph and injects an ambient block (#8).
24
+ *
25
+ * THE TOOLS ARE NOT HERE. Every Gibson tool — findings, knowledge, delegation,
26
+ * componentize, and one per RPC of every SDK service — comes from the Gibson
27
+ * MCP server, which this plugin registers in opencode's own `mcp` block
28
+ * (ADR-0008). opencode spawns it, so the tools reach the model the same way
29
+ * they reach Claude Code, Cursor and the rest, and a new SDK release adds
30
+ * tools with no change here.
31
+ *
32
+ * What is left is what only a plugin can do: the model provider, the ambient
33
+ * knowledge injection, and the session id.
28
34
  *
29
35
  * The plugin fails open throughout. A coding agent that cannot reach its
30
36
  * platform must still be a working coding agent, so every platform failure
package/dist/index.js CHANGED
@@ -2,12 +2,9 @@ import { connectGibson, startCompletionsShim } from "@zeroroot-ai/sdk";
2
2
  import { existsSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
- import { componentizeTool, enrollComponentTool } from "./componentize.js";
6
- import { delegationTools } from "./delegate.js";
7
- import { buildGibsonTools } from "./gibson-tools.js";
8
- import { ambientKnowledge, recallTool } from "./knowledge.js";
5
+ import { ambientKnowledge } from "./knowledge.js";
9
6
  import { selectKnowledgeSource } from "./knowledge-source.js";
10
- import { gibsonFindingsBackend, localFindingsBackend, submitFindingTool, } from "./findings.js";
7
+ import { gibsonMcpServer } from "./mcp-config.js";
11
8
  /**
12
9
  * @zeroroot-ai/zerocool — the main Gibson plugin.
13
10
  *
@@ -29,11 +26,17 @@ import { gibsonFindingsBackend, localFindingsBackend, submitFindingTool, } from
29
26
  * - checks in as a Gibson component (Capability Grant + RegisterComponent + heartbeat),
30
27
  * - starts a local OpenAI-compatible shim over the harness,
31
28
  * - auto-registers a zero-config `gibson` provider via the `config` hook (#6),
32
- * - emits findings into the tenant World (#7),
33
- * - reads the tenant knowledge graph (#8),
34
- * - exposes Gibson tools and plugins as opencode tools (#9),
35
- * - delegates to other agents and drives missions (#10),
36
- * - enrolls produced artifacts into the fleet (#11).
29
+ * - reads the tenant knowledge graph and injects an ambient block (#8).
30
+ *
31
+ * THE TOOLS ARE NOT HERE. Every Gibson tool — findings, knowledge, delegation,
32
+ * componentize, and one per RPC of every SDK service — comes from the Gibson
33
+ * MCP server, which this plugin registers in opencode's own `mcp` block
34
+ * (ADR-0008). opencode spawns it, so the tools reach the model the same way
35
+ * they reach Claude Code, Cursor and the rest, and a new SDK release adds
36
+ * tools with no change here.
37
+ *
38
+ * What is left is what only a plugin can do: the model provider, the ambient
39
+ * knowledge injection, and the session id.
37
40
  *
38
41
  * The plugin fails open throughout. A coding agent that cannot reach its
39
42
  * platform must still be a working coding agent, so every platform failure
@@ -57,12 +60,11 @@ export const GibsonPlugin = async () => {
57
60
  // the operator to keep a spent credential in their environment forever.
58
61
  const checkedIn = existsSync(hostKeyPath);
59
62
  const bootstrapToken = checkedIn ? undefined : process.env.GIBSON_BOOTSTRAP_TOKEN;
60
- // Standalone: findings go to a local log, and componentize still works —
61
- // producing a manifest needs no platform.
62
- const standaloneTools = () => ({
63
- submit_finding: submitFindingTool(localFindingsBackend(process.env.ZEROCOOL_FINDINGS_LOG ?? join(homedir(), ".zerocool", "findings.jsonl")), context),
64
- gibson_componentize: componentizeTool(),
65
- });
63
+ // Standalone: the MCP server still runs. It decides its own posture from
64
+ // what is present, reports it through gibson_status, and offers the tools
65
+ // that need no platform. A coding agent that cannot reach Gibson is still a
66
+ // working coding agent.
67
+ const standalone = () => ({ config: async (config) => addMcpServer(config) });
66
68
  // Standalone unless we can authenticate: a platform URL plus either a token
67
69
  // for the first check-in or an already-registered host key.
68
70
  if (!platformURL || (!bootstrapToken && !checkedIn)) {
@@ -72,7 +74,7 @@ export const GibsonPlugin = async () => {
72
74
  "GIBSON_BOOTSTRAP_TOKEN=<one-time token>. After that the host key at " +
73
75
  `${hostKeyPath} is enough — you can drop the token.`);
74
76
  }
75
- return { tool: standaloneTools() };
77
+ return standalone();
76
78
  }
77
79
  let session;
78
80
  let shim;
@@ -102,34 +104,19 @@ export const GibsonPlugin = async () => {
102
104
  // Fail open to standalone: a coding agent must still work if the platform is unreachable.
103
105
  console.error(`[zerocool] Gibson connect failed; continuing standalone: ${e.message}`);
104
106
  await shim?.close();
105
- return { tool: standaloneTools() };
107
+ return standalone();
106
108
  }
107
109
  const shimUrl = shim.url;
108
110
  const live = session;
109
- // Tool discovery runs once at load — opencode reads the `tool` hook as a
110
- // static object, so a tool registered mid-session would never reach the model.
111
111
  // Which grant this process reads the knowledge graph with. A dispatched run
112
112
  // reads as the TASK; an interactive one keeps the component grant. Chosen once
113
113
  // here so nothing downstream has to ask.
114
114
  const { knowledge, scope: knowledgeScope } = selectKnowledgeSource(live);
115
115
  console.error(`[zerocool] knowledge reads use the ${knowledgeScope} grant`);
116
- const gibson = await buildGibsonTools(live);
117
- if (gibson.note)
118
- console.error(`[zerocool] Gibson tool discovery: ${gibson.note}`);
119
- else
120
- console.error(`[zerocool] ${gibson.discovered} Gibson tool(s) registered`);
121
- const tools = {
122
- submit_finding: submitFindingTool(gibsonFindingsBackend(live), context),
123
- recall: recallTool(knowledge),
124
- gibson_componentize: componentizeTool(),
125
- gibson_enroll_component: enrollComponentTool(live),
126
- ...gibson.tools,
127
- ...delegationTools(live),
128
- };
129
116
  const injectKnowledge = ambientKnowledge(knowledge, process.env.ZEROCOOL_AMBIENT_QUERY ?? "prior findings and security facts for this codebase");
130
117
  return {
131
- tool: tools,
132
- // Zero-config LLM: inject the Gibson provider at config-load time (#6).
118
+ // Zero-config LLM, and the one tool surface: inject both at config-load
119
+ // time (#6, ADR-0008).
133
120
  //
134
121
  // `shimUrl` is always live here, and that is a type-level guarantee rather
135
122
  // than a timing assumption: `Plugin` returns `Promise<Hooks>`, so opencode
@@ -146,13 +133,14 @@ export const GibsonPlugin = async () => {
146
133
  options: { baseURL: shimUrl, apiKey: "gibson" },
147
134
  };
148
135
  }
136
+ addMcpServer(config);
149
137
  },
150
138
  // Ambient knowledge (#8): one cached GraphRAG lookup per session, injected
151
139
  // into the system prompt from the second turn on.
152
140
  "experimental.chat.system.transform": injectKnowledge,
153
- // Track the session so findings carry provenance (#7). This hook does not
154
- // emit findings: a file edit is not a security finding, and inventing one
155
- // would fill the tenant graph with noise a human then has to triage.
141
+ // Track the session id, which the ambient block and any local state key
142
+ // off. This hook emits nothing: a file edit is not a security finding, and
143
+ // inventing one would fill the tenant graph with noise a human must triage.
156
144
  event: async ({ event }) => {
157
145
  const sessionID = event.properties?.sessionID;
158
146
  if (sessionID)
@@ -164,4 +152,11 @@ export const GibsonPlugin = async () => {
164
152
  },
165
153
  };
166
154
  };
155
+ /** Register the Gibson MCP server in opencode's own `mcp` block. */
156
+ function addMcpServer(config) {
157
+ const cfg = config;
158
+ cfg.mcp = cfg.mcp ?? {};
159
+ if (!cfg.mcp.gibson)
160
+ cfg.mcp.gibson = gibsonMcpServer();
161
+ }
167
162
  export default GibsonPlugin;
@@ -1,13 +1,4 @@
1
- import type { ToolDefinition } from "@opencode-ai/plugin";
2
1
  import { type KnowledgeSource } from "@zeroroot-ai/sdk";
3
- export declare function recallTool(knowledge: KnowledgeSource): ToolDefinition;
4
- /**
5
- * Per-session ambient knowledge for the system prompt.
6
- *
7
- * Returns a transform that injects a short context block once a session has
8
- * produced one. The lookup is fired once and cached, so a session pays for at
9
- * most one GraphRAG query no matter how many turns it runs.
10
- */
11
2
  export declare function ambientKnowledge(knowledge: KnowledgeSource, seedQuery: string): (input: {
12
3
  sessionID?: string;
13
4
  }, output: {
package/dist/knowledge.js CHANGED
@@ -1,5 +1,4 @@
1
- import { tool } from "@opencode-ai/plugin";
2
- import { formatKnowledgeForPrompt, isSeamUnavailable, } from "@zeroroot-ai/sdk";
1
+ import { formatKnowledgeForPrompt, } from "@zeroroot-ai/sdk";
3
2
  /**
4
3
  * Knowledge — read the tenant graph so the agent sharpens across runs
5
4
  * (zerocool-plugins#8).
@@ -22,57 +21,6 @@ import { formatKnowledgeForPrompt, isSeamUnavailable, } from "@zeroroot-ai/sdk";
22
21
  */
23
22
  /** How many hits the ambient block injects. Kept small — it is prompt overhead. */
24
23
  const AMBIENT_LIMIT = 5;
25
- export function recallTool(knowledge) {
26
- const z = tool.schema;
27
- return tool({
28
- description: "Search this tenant's Gibson knowledge graph for prior findings, targets and " +
29
- "security facts recorded by earlier runs. Use it before starting work on an " +
30
- "unfamiliar area, or to check whether an issue was already reported.",
31
- args: {
32
- query: z.string().describe("What to look for, in natural language."),
33
- limit: z.number().min(1).max(50).optional().describe("Maximum hits to return. Defaults to 10."),
34
- node_types: z
35
- .array(z.string())
36
- .optional()
37
- .describe('Restrict to node types, e.g. ["finding"] or ["host", "technique"].'),
38
- },
39
- async execute(args) {
40
- let hits;
41
- try {
42
- hits = await knowledge.query({
43
- text: args.query,
44
- topK: args.limit ?? 10,
45
- ...(args.node_types ? { nodeTypes: args.node_types } : {}),
46
- });
47
- }
48
- catch (e) {
49
- if (isSeamUnavailable(e)) {
50
- return {
51
- title: "recall unavailable",
52
- output: "The Gibson knowledge graph is not available on this daemon: the GraphRAG " +
53
- "seam is not wired (gibson#1186). Continue without prior context.",
54
- };
55
- }
56
- throw e;
57
- }
58
- if (hits.length === 0) {
59
- return { title: "no matches", output: `Nothing in the tenant graph matches "${args.query}".` };
60
- }
61
- return {
62
- title: `${hits.length} match${hits.length === 1 ? "" : "es"}`,
63
- output: formatKnowledgeForPrompt(hits),
64
- metadata: { hit_count: hits.length, node_types: [...new Set(hits.map((h) => h.type))] },
65
- };
66
- },
67
- });
68
- }
69
- /**
70
- * Per-session ambient knowledge for the system prompt.
71
- *
72
- * Returns a transform that injects a short context block once a session has
73
- * produced one. The lookup is fired once and cached, so a session pays for at
74
- * most one GraphRAG query no matter how many turns it runs.
75
- */
76
24
  export function ambientKnowledge(knowledge, seedQuery) {
77
25
  const cache = new Map();
78
26
  const inFlight = new Map();
@@ -0,0 +1,20 @@
1
+ /**
2
+ * How opencode reaches the Gibson MCP server (ADR-0008, slice A7).
3
+ *
4
+ * opencode spawns a local MCP server from its `mcp` block, the same way every
5
+ * other host does from its own config file. The plugin writes that entry in
6
+ * the `config` hook, so a user configures nothing.
7
+ *
8
+ * The command is the published package, not a path inside this plugin: the
9
+ * server is shared by every host and released on the SDK train, so pinning it
10
+ * to whatever this plugin happens to bundle would freeze the tool set.
11
+ */
12
+ export declare const GIBSON_MCP_PACKAGE = "@zeroroot-ai/gibson-mcp@latest";
13
+ export interface OpencodeMcpLocal {
14
+ type: "local";
15
+ command: string[];
16
+ enabled: boolean;
17
+ environment?: Record<string, string>;
18
+ }
19
+ /** The `mcp.gibson` entry. `environment` carries nothing secret. */
20
+ export declare function gibsonMcpServer(env?: NodeJS.ProcessEnv): OpencodeMcpLocal;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * How opencode reaches the Gibson MCP server (ADR-0008, slice A7).
3
+ *
4
+ * opencode spawns a local MCP server from its `mcp` block, the same way every
5
+ * other host does from its own config file. The plugin writes that entry in
6
+ * the `config` hook, so a user configures nothing.
7
+ *
8
+ * The command is the published package, not a path inside this plugin: the
9
+ * server is shared by every host and released on the SDK train, so pinning it
10
+ * to whatever this plugin happens to bundle would freeze the tool set.
11
+ */
12
+ export const GIBSON_MCP_PACKAGE = "@zeroroot-ai/gibson-mcp@latest";
13
+ /** The `mcp.gibson` entry. `environment` carries nothing secret. */
14
+ export function gibsonMcpServer(env = process.env) {
15
+ const environment = {};
16
+ // Addressing only. The server does its own check-in, and a grant is never
17
+ // written into a config file.
18
+ for (const name of ["GIBSON_PLATFORM_URL", "GIBSON_DAEMON_URL", "GIBSON_TARGET_ID", "GIBSON_CA_CERT"]) {
19
+ const value = env[name];
20
+ if (value)
21
+ environment[name] = value;
22
+ }
23
+ return {
24
+ type: "local",
25
+ command: ["npx", "--yes", "--package", GIBSON_MCP_PACKAGE, "gibson-mcp", "--transport", "stdio"],
26
+ enabled: true,
27
+ ...(Object.keys(environment).length > 0 ? { environment } : {}),
28
+ };
29
+ }
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;