@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.
- package/dist/dispatch.d.ts +2 -1
- package/dist/dispatch.js +22 -6
- package/dist/findings.d.ts +21 -2
- package/dist/findings.js +1 -80
- package/dist/index.d.ts +11 -5
- package/dist/index.js +33 -38
- package/dist/knowledge.d.ts +0 -9
- package/dist/knowledge.js +1 -53
- package/dist/mcp-config.d.ts +20 -0
- package/dist/mcp-config.js +29 -0
- package/dist/watch.d.ts +47 -33
- package/dist/watch.js +59 -69
- package/package.json +3 -3
- package/dist/componentize.d.ts +0 -10
- package/dist/componentize.js +0 -149
- package/dist/delegate.d.ts +0 -11
- package/dist/delegate.js +0 -216
- package/dist/gibson-tools.d.ts +0 -45
- package/dist/gibson-tools.js +0 -175
package/dist/dispatch.d.ts
CHANGED
|
@@ -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/findings.d.ts
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
|
-
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
1
|
import { type Finding, type GibsonSession, type TaskHarness } from "@zeroroot-ai/sdk";
|
|
2
|
+
/**
|
|
3
|
+
* Findings — emit what the agent discovers into the tenant World
|
|
4
|
+
* (zerocool-plugins#7).
|
|
5
|
+
*
|
|
6
|
+
* One code path, two backends. With a Gibson session a finding goes to the
|
|
7
|
+
* tenant graph through `ComponentService.SubmitFinding` under the agent's
|
|
8
|
+
* COMPONENT identity; standalone it is appended to a local JSONL log. The tool
|
|
9
|
+
* the model sees is identical either way, so a prompt that works standalone
|
|
10
|
+
* works on the platform.
|
|
11
|
+
*
|
|
12
|
+
* The agent decides what is a finding. Nothing here invents findings from file
|
|
13
|
+
* edits or message traffic: a `file.edited` event is not a security finding, and
|
|
14
|
+
* auto-emitting one would fill the tenant graph with noise that a human then has
|
|
15
|
+
* to triage.
|
|
16
|
+
*
|
|
17
|
+
* The `submit_finding` tool the model calls lives in the Gibson MCP server now
|
|
18
|
+
* (ADR-0008). What is left here is the backend the dispatched task kinds use
|
|
19
|
+
* when they submit a finding themselves, with no model in the loop:
|
|
20
|
+
* `source-analysis` triages semgrep candidates, and `dispatch` reports them
|
|
21
|
+
* under the task grant.
|
|
22
|
+
*/
|
|
3
23
|
export interface FindingsBackend {
|
|
4
24
|
/** Submit a finding; returns the identifier it was recorded under. */
|
|
5
25
|
submit(f: Finding): Promise<string>;
|
|
@@ -32,4 +52,3 @@ export interface SessionContext {
|
|
|
32
52
|
* `agent_name` is fixed to "zerocool" rather than exposed as an argument — a
|
|
33
53
|
* model must not be able to attribute its findings to another agent.
|
|
34
54
|
*/
|
|
35
|
-
export declare function submitFindingTool(backend: FindingsBackend, context: SessionContext): ToolDefinition;
|
package/dist/findings.js
CHANGED
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
import {
|
|
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
|
-
* -
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
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 {
|
|
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 {
|
|
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
|
-
* -
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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:
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
|
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
|
|
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:
|
|
132
|
-
//
|
|
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
|
|
154
|
-
//
|
|
155
|
-
// would fill the tenant graph with noise a human
|
|
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;
|
package/dist/knowledge.d.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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;
|