@zeroroot-ai/zerocool 0.8.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/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/package.json +2 -2
- 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/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/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeroroot-ai/zerocool",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "opencode
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "opencode host adapter for Gibson (zerocool): zero-config LLM through the harness, ambient knowledge, and the Gibson MCP server registered for you. The tools come from @zeroroot-ai/gibson-mcp.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/dist/componentize.d.ts
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
-
import { type GibsonSession } from "@zeroroot-ai/sdk";
|
|
3
|
-
/**
|
|
4
|
-
* Write a validated component manifest to disk. Available with or without a
|
|
5
|
-
* Gibson session — the manifest is the artifact a build pipeline needs, and
|
|
6
|
-
* producing it must not require a platform connection.
|
|
7
|
-
*/
|
|
8
|
-
export declare function componentizeTool(): ToolDefinition;
|
|
9
|
-
/** Register a produced artifact with the tenant registry. */
|
|
10
|
-
export declare function enrollComponentTool(session: GibsonSession): ToolDefinition;
|
package/dist/componentize.js
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
import { buildComponentManifest, enrollComponent, enrollmentSupported, validateComponentSpec, } from "@zeroroot-ai/sdk";
|
|
3
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
|
-
import { dirname, resolve } from "node:path";
|
|
5
|
-
/**
|
|
6
|
-
* Componentize produced artifacts into the tenant fleet (zerocool-plugins#11).
|
|
7
|
-
*
|
|
8
|
-
* The factory payoff: an artifact the agent built becomes a registered Gibson
|
|
9
|
-
* component. The contract is language-neutral — `RegisterComponent` describes a
|
|
10
|
-
* component by kind, name, version and contract fields, never by the language
|
|
11
|
-
* it was written in.
|
|
12
|
-
*
|
|
13
|
-
* Two tools, because the work splits at a real boundary:
|
|
14
|
-
*
|
|
15
|
-
* - `gibson_componentize` writes the manifest to disk without touching the
|
|
16
|
-
* platform. It is the step that can always run, and the artefact it produces
|
|
17
|
-
* is what a build pipeline consumes.
|
|
18
|
-
* - `gibson_enroll_component` registers the manifest with the daemon.
|
|
19
|
-
*
|
|
20
|
-
* WHAT THIS DOES NOT DO. Image build and publish is a build-system concern, not
|
|
21
|
-
* an RPC; the OCI reference is an input here, carried in `metadata.image`. And
|
|
22
|
-
* enrollment currently registers under the CALLING agent's identity — the
|
|
23
|
-
* policy-bounded RPC that would give a produced component its own short-TTL
|
|
24
|
-
* credential is gibson#1185 and does not exist yet. The tool says so in its
|
|
25
|
-
* output rather than implying the artifact is independently dispatchable.
|
|
26
|
-
*/
|
|
27
|
-
const KINDS = ["agent", "tool", "plugin"];
|
|
28
|
-
/** Shared argument shape for both tools. */
|
|
29
|
-
function specArgs() {
|
|
30
|
-
const z = tool.schema;
|
|
31
|
-
return {
|
|
32
|
-
kind: z.enum(KINDS).describe("Component kind."),
|
|
33
|
-
name: z.string().describe("Component name, as it will appear in the tenant registry."),
|
|
34
|
-
version: z.string().describe('Semantic version of the artifact, e.g. "0.1.0".'),
|
|
35
|
-
image: z
|
|
36
|
-
.string()
|
|
37
|
-
.optional()
|
|
38
|
-
.describe("OCI image reference of the built artifact. Required before it can be dispatched."),
|
|
39
|
-
language: z.string().optional().describe("Language the artifact is written in, for the catalog."),
|
|
40
|
-
capabilities: z
|
|
41
|
-
.array(z.string())
|
|
42
|
-
.optional()
|
|
43
|
-
.describe('Agent capabilities. Required when kind is "agent".'),
|
|
44
|
-
methods: z.array(z.string()).optional().describe('Plugin method names. Required when kind is "plugin".'),
|
|
45
|
-
input_message_type: z
|
|
46
|
-
.string()
|
|
47
|
-
.optional()
|
|
48
|
-
.describe('Fully-qualified proto input type. Required when kind is "tool".'),
|
|
49
|
-
output_message_type: z
|
|
50
|
-
.string()
|
|
51
|
-
.optional()
|
|
52
|
-
.describe('Fully-qualified proto output type. Required when kind is "tool".'),
|
|
53
|
-
config_schema_json: z.string().optional().describe("JSON Schema for a plugin's configuration."),
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
/** Build a ComponentSpec from tool arguments. */
|
|
57
|
-
function toSpec(args) {
|
|
58
|
-
const metadata = {};
|
|
59
|
-
if (args.image)
|
|
60
|
-
metadata.image = args.image;
|
|
61
|
-
if (args.language)
|
|
62
|
-
metadata.language = args.language;
|
|
63
|
-
return {
|
|
64
|
-
kind: args.kind,
|
|
65
|
-
name: args.name,
|
|
66
|
-
version: args.version,
|
|
67
|
-
metadata,
|
|
68
|
-
...(args.capabilities ? { capabilities: args.capabilities } : {}),
|
|
69
|
-
...(args.methods ? { methods: args.methods } : {}),
|
|
70
|
-
...(args.input_message_type ? { inputMessageType: args.input_message_type } : {}),
|
|
71
|
-
...(args.output_message_type ? { outputMessageType: args.output_message_type } : {}),
|
|
72
|
-
...(args.config_schema_json ? { configSchemaJson: args.config_schema_json } : {}),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Write a validated component manifest to disk. Available with or without a
|
|
77
|
-
* Gibson session — the manifest is the artifact a build pipeline needs, and
|
|
78
|
-
* producing it must not require a platform connection.
|
|
79
|
-
*/
|
|
80
|
-
export function componentizeTool() {
|
|
81
|
-
const z = tool.schema;
|
|
82
|
-
return tool({
|
|
83
|
-
description: "Turn a built artifact into a Gibson component manifest and write it to disk. " +
|
|
84
|
-
"Use this after building a tool or agent you want to add to the fleet. Validates " +
|
|
85
|
-
"the artifact against Gibson's component contract; does not build or push an image.",
|
|
86
|
-
args: {
|
|
87
|
-
...specArgs(),
|
|
88
|
-
path: z
|
|
89
|
-
.string()
|
|
90
|
-
.optional()
|
|
91
|
-
.describe('Where to write the manifest. Defaults to "gibson-component.json".'),
|
|
92
|
-
},
|
|
93
|
-
async execute(args, ctx) {
|
|
94
|
-
const spec = toSpec(args);
|
|
95
|
-
const problems = validateComponentSpec(spec);
|
|
96
|
-
if (problems.length > 0) {
|
|
97
|
-
return {
|
|
98
|
-
title: "invalid component spec",
|
|
99
|
-
output: `This artifact does not satisfy Gibson's ${spec.kind} contract:\n${problems.map((p) => `- ${p}`).join("\n")}`,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
const manifest = buildComponentManifest(spec);
|
|
103
|
-
const target = resolve(ctx.directory, args.path ?? "gibson-component.json");
|
|
104
|
-
await mkdir(dirname(target), { recursive: true });
|
|
105
|
-
await writeFile(target, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
106
|
-
const missingImage = spec.metadata?.image
|
|
107
|
-
? ""
|
|
108
|
-
: "\n\nNo image reference was given. Build and push an image, then re-run with " +
|
|
109
|
-
"image=<oci-ref> before enrolling — a component without an image cannot be dispatched.";
|
|
110
|
-
return {
|
|
111
|
-
title: `${spec.kind} manifest: ${spec.name}`,
|
|
112
|
-
output: `Wrote a valid ${spec.kind} manifest for ${spec.name}@${spec.version} to ${target}.${missingImage}`,
|
|
113
|
-
metadata: { path: target, kind: spec.kind, name: spec.name },
|
|
114
|
-
};
|
|
115
|
-
},
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
/** Register a produced artifact with the tenant registry. */
|
|
119
|
-
export function enrollComponentTool(session) {
|
|
120
|
-
return tool({
|
|
121
|
-
description: "Register a produced artifact with Gibson so it joins the tenant fleet. Run " +
|
|
122
|
-
"gibson_componentize first to check the artifact against the component contract.",
|
|
123
|
-
args: specArgs(),
|
|
124
|
-
async execute(args) {
|
|
125
|
-
const spec = toSpec(args);
|
|
126
|
-
const problems = validateComponentSpec(spec);
|
|
127
|
-
if (problems.length > 0) {
|
|
128
|
-
return {
|
|
129
|
-
title: "invalid component spec",
|
|
130
|
-
output: `This artifact does not satisfy Gibson's ${spec.kind} contract:\n${problems.map((p) => `- ${p}`).join("\n")}`,
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
const result = await enrollComponent(session.clients.component, spec);
|
|
134
|
-
const { reason } = enrollmentSupported();
|
|
135
|
-
return {
|
|
136
|
-
title: `enrolled ${spec.name}`,
|
|
137
|
-
output: `Registered ${spec.kind} ${spec.name}@${spec.version} as instance ${result.instanceId}.\n\n` +
|
|
138
|
-
`Note: ${reason}. It is visible in the tenant registry, but it will drop out ` +
|
|
139
|
-
`when heartbeats stop (every ${Math.round(result.heartbeatIntervalMs / 1000)}s) ` +
|
|
140
|
-
`unless the artifact itself runs and checks in.`,
|
|
141
|
-
metadata: {
|
|
142
|
-
instance_id: result.instanceId,
|
|
143
|
-
has_own_identity: result.hasOwnIdentity,
|
|
144
|
-
kind: spec.kind,
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
},
|
|
148
|
-
});
|
|
149
|
-
}
|
package/dist/delegate.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
-
import { type GibsonSession } from "@zeroroot-ai/sdk";
|
|
3
|
-
export declare function listAgentsTool(session: GibsonSession): ToolDefinition;
|
|
4
|
-
export declare function delegateTool(session: GibsonSession): ToolDefinition;
|
|
5
|
-
export declare function createMissionTool(session: GibsonSession): ToolDefinition;
|
|
6
|
-
export declare function runMissionTool(session: GibsonSession): ToolDefinition;
|
|
7
|
-
export declare function missionStatusTool(session: GibsonSession): ToolDefinition;
|
|
8
|
-
export declare function missionResultsTool(session: GibsonSession): ToolDefinition;
|
|
9
|
-
export declare function cancelMissionTool(session: GibsonSession): ToolDefinition;
|
|
10
|
-
/** Every delegation and mission tool, keyed for the opencode `tool` hook. */
|
|
11
|
-
export declare function delegationTools(session: GibsonSession): Record<string, ToolDefinition>;
|
package/dist/delegate.js
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
import { cancelMission, createMission, delegateToAgent, getMissionResults, getMissionStatus, isSeamUnavailable, listAgents, newTask, runMission, waitMission, } from "@zeroroot-ai/sdk";
|
|
3
|
-
/**
|
|
4
|
-
* Delegation and missions — the factory's dispatch and recursion
|
|
5
|
-
* (zerocool-plugins#10).
|
|
6
|
-
*
|
|
7
|
-
* Rules-of-engagement, authz and budget denials come back as daemon errors.
|
|
8
|
-
* They are surfaced verbatim to the model rather than retried: a denial is a
|
|
9
|
-
* policy decision, and an agent that retries a denied delegation is just
|
|
10
|
-
* burning budget against a wall.
|
|
11
|
-
*
|
|
12
|
-
* Every RPC here needs the daemon's `agentDelegator` / `missionMgr` seams,
|
|
13
|
-
* which are not wired (gibson#1186). Each tool reports that state plainly
|
|
14
|
-
* instead of surfacing a bare "unimplemented" to the model.
|
|
15
|
-
*/
|
|
16
|
-
const UNWIRED = "This daemon has no agent delegation or mission management wired (gibson#1186). " +
|
|
17
|
-
"Complete the work directly instead.";
|
|
18
|
-
export function listAgentsTool(session) {
|
|
19
|
-
return tool({
|
|
20
|
-
description: "List the Gibson agents in this tenant that work can be delegated to, with " +
|
|
21
|
-
"their capabilities and the target types they handle.",
|
|
22
|
-
args: {},
|
|
23
|
-
async execute() {
|
|
24
|
-
const { agents, unavailable } = await listAgents(session.clients.component);
|
|
25
|
-
if (unavailable)
|
|
26
|
-
return { title: "delegation unavailable", output: UNWIRED };
|
|
27
|
-
if (agents.length === 0) {
|
|
28
|
-
return { title: "no agents", output: "No agents are registered for this tenant." };
|
|
29
|
-
}
|
|
30
|
-
const lines = agents.map((a) => {
|
|
31
|
-
const caps = a.capabilities.length > 0 ? ` — capabilities: ${a.capabilities.join(", ")}` : "";
|
|
32
|
-
const targets = a.targetTypes.length > 0 ? ` — targets: ${a.targetTypes.join(", ")}` : "";
|
|
33
|
-
return `- ${a.name} (${a.version}) ${a.description}${caps}${targets}`;
|
|
34
|
-
});
|
|
35
|
-
return {
|
|
36
|
-
title: `${agents.length} agent${agents.length === 1 ? "" : "s"}`,
|
|
37
|
-
output: lines.join("\n"),
|
|
38
|
-
metadata: { agent_count: agents.length },
|
|
39
|
-
};
|
|
40
|
-
},
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
export function delegateTool(session) {
|
|
44
|
-
const z = tool.schema;
|
|
45
|
-
return tool({
|
|
46
|
-
description: "Delegate a sub-task to another Gibson agent and wait for its result. Use " +
|
|
47
|
-
"gibson_list_agents first to pick an agent whose capabilities match the task.",
|
|
48
|
-
args: {
|
|
49
|
-
agent: z.string().describe("Name of the agent to delegate to."),
|
|
50
|
-
goal: z.string().describe("What the delegate should accomplish."),
|
|
51
|
-
context: z
|
|
52
|
-
.record(z.string(), z.any())
|
|
53
|
-
.optional()
|
|
54
|
-
.describe("Target details, prior findings, or other context the delegate needs."),
|
|
55
|
-
max_turns: z.number().optional().describe("Cap on the delegate's LLM turns."),
|
|
56
|
-
allowed_tools: z.array(z.string()).optional().describe("Restrict the delegate to these tools."),
|
|
57
|
-
},
|
|
58
|
-
async execute(args) {
|
|
59
|
-
const constraints = {
|
|
60
|
-
...(args.max_turns ? { MaxTurns: args.max_turns } : {}),
|
|
61
|
-
...(args.allowed_tools ? { AllowedTools: args.allowed_tools } : {}),
|
|
62
|
-
};
|
|
63
|
-
const task = newTask(args.goal, {
|
|
64
|
-
...(args.context ? { Context: args.context } : {}),
|
|
65
|
-
...(Object.keys(constraints).length > 0 ? { Constraints: constraints } : {}),
|
|
66
|
-
});
|
|
67
|
-
try {
|
|
68
|
-
const result = await delegateToAgent(session.clients.component, args.agent, task);
|
|
69
|
-
const output = typeof result.Output === "string" ? result.Output : JSON.stringify(result.Output, null, 2);
|
|
70
|
-
const findings = result.Findings && result.Findings.length > 0
|
|
71
|
-
? `\n\nFindings recorded: ${result.Findings.join(", ")}`
|
|
72
|
-
: "";
|
|
73
|
-
const failure = result.error?.message ? `\n\nError: ${result.error.message}` : "";
|
|
74
|
-
return {
|
|
75
|
-
title: `${args.agent}: ${result.Status}`,
|
|
76
|
-
output: `${output ?? "(no output)"}${findings}${failure}`,
|
|
77
|
-
metadata: { status: result.Status, finding_ids: result.Findings ?? [] },
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
catch (e) {
|
|
81
|
-
if (isSeamUnavailable(e))
|
|
82
|
-
return { title: "delegation unavailable", output: UNWIRED };
|
|
83
|
-
// Authz / RoE / budget denials land here. Show the daemon's reason.
|
|
84
|
-
return {
|
|
85
|
-
title: `${args.agent}: denied`,
|
|
86
|
-
output: `Delegation to ${args.agent} was refused: ${e.message}`,
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
export function createMissionTool(session) {
|
|
93
|
-
const z = tool.schema;
|
|
94
|
-
return tool({
|
|
95
|
-
description: "Create a Gibson mission from a mission definition, bound to a target. The " +
|
|
96
|
-
"mission is not started — call gibson_run_mission to queue it.",
|
|
97
|
-
args: {
|
|
98
|
-
definition: z.record(z.string(), z.any()).describe("Mission definition object."),
|
|
99
|
-
target_id: z.string().describe("Identifier of the target the mission runs against."),
|
|
100
|
-
opts: z.record(z.string(), z.any()).optional().describe("Optional mission creation options."),
|
|
101
|
-
},
|
|
102
|
-
async execute(args) {
|
|
103
|
-
try {
|
|
104
|
-
const info = await createMission(session.clients.component, args.definition, args.target_id, args.opts);
|
|
105
|
-
const id = (info.id ?? info.mission_id ?? info.missionId);
|
|
106
|
-
return {
|
|
107
|
-
title: id ? `mission ${id}` : "mission created",
|
|
108
|
-
output: JSON.stringify(info, null, 2),
|
|
109
|
-
...(id ? { metadata: { mission_id: id } } : {}),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
catch (e) {
|
|
113
|
-
if (isSeamUnavailable(e))
|
|
114
|
-
return { title: "missions unavailable", output: UNWIRED };
|
|
115
|
-
throw e;
|
|
116
|
-
}
|
|
117
|
-
},
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
|
-
export function runMissionTool(session) {
|
|
121
|
-
const z = tool.schema;
|
|
122
|
-
return tool({
|
|
123
|
-
description: "Queue a created Gibson mission for execution. Returns as soon as it is queued.",
|
|
124
|
-
args: { mission_id: z.string().describe("Mission to run.") },
|
|
125
|
-
async execute(args) {
|
|
126
|
-
try {
|
|
127
|
-
await runMission(session.clients.component, args.mission_id);
|
|
128
|
-
return { title: "queued", output: `Mission ${args.mission_id} is queued for execution.` };
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
if (isSeamUnavailable(e))
|
|
132
|
-
return { title: "missions unavailable", output: UNWIRED };
|
|
133
|
-
throw e;
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
export function missionStatusTool(session) {
|
|
139
|
-
const z = tool.schema;
|
|
140
|
-
return tool({
|
|
141
|
-
description: "Check a Gibson mission's status, or block until it finishes. Use wait=true " +
|
|
142
|
-
"only when you intend to stop and wait for the result.",
|
|
143
|
-
args: {
|
|
144
|
-
mission_id: z.string().describe("Mission to inspect."),
|
|
145
|
-
wait: z.boolean().optional().describe("Block until the mission reaches a terminal state."),
|
|
146
|
-
timeout_ms: z
|
|
147
|
-
.number()
|
|
148
|
-
.optional()
|
|
149
|
-
.describe("How long to block when wait is true. Defaults to 5 minutes."),
|
|
150
|
-
},
|
|
151
|
-
async execute(args) {
|
|
152
|
-
try {
|
|
153
|
-
const state = args.wait
|
|
154
|
-
? await waitMission(session.clients.component, args.mission_id, args.timeout_ms ?? 300_000)
|
|
155
|
-
: await getMissionStatus(session.clients.component, args.mission_id);
|
|
156
|
-
return {
|
|
157
|
-
title: `mission ${args.mission_id}`,
|
|
158
|
-
output: JSON.stringify(state, null, 2),
|
|
159
|
-
};
|
|
160
|
-
}
|
|
161
|
-
catch (e) {
|
|
162
|
-
if (isSeamUnavailable(e))
|
|
163
|
-
return { title: "missions unavailable", output: UNWIRED };
|
|
164
|
-
throw e;
|
|
165
|
-
}
|
|
166
|
-
},
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
export function missionResultsTool(session) {
|
|
170
|
-
const z = tool.schema;
|
|
171
|
-
return tool({
|
|
172
|
-
description: "Read the final results of a completed Gibson mission.",
|
|
173
|
-
args: { mission_id: z.string().describe("Mission whose results to read.") },
|
|
174
|
-
async execute(args) {
|
|
175
|
-
try {
|
|
176
|
-
const results = await getMissionResults(session.clients.component, args.mission_id);
|
|
177
|
-
return { title: `results ${args.mission_id}`, output: JSON.stringify(results, null, 2) };
|
|
178
|
-
}
|
|
179
|
-
catch (e) {
|
|
180
|
-
if (isSeamUnavailable(e))
|
|
181
|
-
return { title: "missions unavailable", output: UNWIRED };
|
|
182
|
-
throw e;
|
|
183
|
-
}
|
|
184
|
-
},
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
export function cancelMissionTool(session) {
|
|
188
|
-
const z = tool.schema;
|
|
189
|
-
return tool({
|
|
190
|
-
description: "Request cancellation of a running Gibson mission.",
|
|
191
|
-
args: { mission_id: z.string().describe("Mission to cancel.") },
|
|
192
|
-
async execute(args) {
|
|
193
|
-
try {
|
|
194
|
-
await cancelMission(session.clients.component, args.mission_id);
|
|
195
|
-
return { title: "cancelled", output: `Cancellation requested for mission ${args.mission_id}.` };
|
|
196
|
-
}
|
|
197
|
-
catch (e) {
|
|
198
|
-
if (isSeamUnavailable(e))
|
|
199
|
-
return { title: "missions unavailable", output: UNWIRED };
|
|
200
|
-
throw e;
|
|
201
|
-
}
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
/** Every delegation and mission tool, keyed for the opencode `tool` hook. */
|
|
206
|
-
export function delegationTools(session) {
|
|
207
|
-
return {
|
|
208
|
-
gibson_list_agents: listAgentsTool(session),
|
|
209
|
-
gibson_delegate: delegateTool(session),
|
|
210
|
-
gibson_create_mission: createMissionTool(session),
|
|
211
|
-
gibson_run_mission: runMissionTool(session),
|
|
212
|
-
gibson_mission_status: missionStatusTool(session),
|
|
213
|
-
gibson_mission_results: missionResultsTool(session),
|
|
214
|
-
gibson_cancel_mission: cancelMissionTool(session),
|
|
215
|
-
};
|
|
216
|
-
}
|
package/dist/gibson-tools.d.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
-
import { type GibsonSession, type GibsonTool } from "@zeroroot-ai/sdk";
|
|
3
|
-
/**
|
|
4
|
-
* Gibson tools and plugins as opencode tools (zerocool-plugins#9).
|
|
5
|
-
*
|
|
6
|
-
* Discovered tools are registered **individually** — one opencode tool per
|
|
7
|
-
* Gibson tool, named `gibson_<tool>` — so the model sees them in its tool list
|
|
8
|
-
* with their own descriptions, rather than having to know that a generic
|
|
9
|
-
* dispatcher exists and what to pass it.
|
|
10
|
-
*
|
|
11
|
-
* ON SCHEMAS — the catalog carries no JSON Schema. `ToolDescriptorProto` only
|
|
12
|
-
* names the proto message types of a tool's input and output
|
|
13
|
-
* (`gibson.tool.v1.ScanRequest`), and there is no RPC that resolves those to a
|
|
14
|
-
* schema. So a per-tool wrapper takes a single free-form `input` object and
|
|
15
|
-
* names the expected message type in its description. Inventing a Zod schema
|
|
16
|
-
* from a message type name would be a guess, and a wrong guess would reject
|
|
17
|
-
* valid input before it ever reached the tool.
|
|
18
|
-
*
|
|
19
|
-
* A generic `gibson_call_tool` is always registered as well: discovery is
|
|
20
|
-
* `Unimplemented` on the current daemon (gibson#1186), and without it there
|
|
21
|
-
* would be no way to reach a tool whose name the operator already knows.
|
|
22
|
-
*/
|
|
23
|
-
/** Sanitise a Gibson tool name into an opencode tool key. */
|
|
24
|
-
export declare function toolKey(name: string): string;
|
|
25
|
-
/** Wrap one discovered Gibson tool as an opencode tool. */
|
|
26
|
-
export declare function wrapGibsonTool(session: GibsonSession, t: GibsonTool): ToolDefinition;
|
|
27
|
-
/** The always-present dispatcher for tools the catalog did not surface. */
|
|
28
|
-
export declare function callToolTool(session: GibsonSession): ToolDefinition;
|
|
29
|
-
/** List what Gibson exposes to this tenant — tools and enabled plugins. */
|
|
30
|
-
export declare function listToolsTool(session: GibsonSession): ToolDefinition;
|
|
31
|
-
/** Invoke a method on an enabled Gibson plugin. */
|
|
32
|
-
export declare function queryPluginTool(session: GibsonSession): ToolDefinition;
|
|
33
|
-
/**
|
|
34
|
-
* Discover the tenant's tools and build one opencode tool for each, plus the
|
|
35
|
-
* always-present dispatcher, catalog and plugin tools.
|
|
36
|
-
*
|
|
37
|
-
* Discovery runs once at plugin load. opencode reads the `tool` hook as a
|
|
38
|
-
* static object, so a tool registered later in the session would not be visible
|
|
39
|
-
* to the model anyway.
|
|
40
|
-
*/
|
|
41
|
-
export declare function buildGibsonTools(session: GibsonSession): Promise<{
|
|
42
|
-
tools: Record<string, ToolDefinition>;
|
|
43
|
-
discovered: number;
|
|
44
|
-
note?: string;
|
|
45
|
-
}>;
|
package/dist/gibson-tools.js
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
import { callGibsonTool, listGibsonPlugins, listGibsonTools, queryGibsonPlugin, } from "@zeroroot-ai/sdk";
|
|
3
|
-
/**
|
|
4
|
-
* Gibson tools and plugins as opencode tools (zerocool-plugins#9).
|
|
5
|
-
*
|
|
6
|
-
* Discovered tools are registered **individually** — one opencode tool per
|
|
7
|
-
* Gibson tool, named `gibson_<tool>` — so the model sees them in its tool list
|
|
8
|
-
* with their own descriptions, rather than having to know that a generic
|
|
9
|
-
* dispatcher exists and what to pass it.
|
|
10
|
-
*
|
|
11
|
-
* ON SCHEMAS — the catalog carries no JSON Schema. `ToolDescriptorProto` only
|
|
12
|
-
* names the proto message types of a tool's input and output
|
|
13
|
-
* (`gibson.tool.v1.ScanRequest`), and there is no RPC that resolves those to a
|
|
14
|
-
* schema. So a per-tool wrapper takes a single free-form `input` object and
|
|
15
|
-
* names the expected message type in its description. Inventing a Zod schema
|
|
16
|
-
* from a message type name would be a guess, and a wrong guess would reject
|
|
17
|
-
* valid input before it ever reached the tool.
|
|
18
|
-
*
|
|
19
|
-
* A generic `gibson_call_tool` is always registered as well: discovery is
|
|
20
|
-
* `Unimplemented` on the current daemon (gibson#1186), and without it there
|
|
21
|
-
* would be no way to reach a tool whose name the operator already knows.
|
|
22
|
-
*/
|
|
23
|
-
/** Sanitise a Gibson tool name into an opencode tool key. */
|
|
24
|
-
export function toolKey(name) {
|
|
25
|
-
return `gibson_${name.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
26
|
-
}
|
|
27
|
-
/** Wrap one discovered Gibson tool as an opencode tool. */
|
|
28
|
-
export function wrapGibsonTool(session, t) {
|
|
29
|
-
const z = tool.schema;
|
|
30
|
-
const schemaNote = t.inputMessageType
|
|
31
|
-
? ` Input must match the Gibson message type ${t.inputMessageType}.`
|
|
32
|
-
: "";
|
|
33
|
-
return tool({
|
|
34
|
-
description: `${t.description || `Gibson tool "${t.name}"`}${schemaNote} Runs through the Gibson harness (authorized and metered).`,
|
|
35
|
-
args: {
|
|
36
|
-
input: z
|
|
37
|
-
.record(z.string(), z.any())
|
|
38
|
-
.describe(`Input object for the ${t.name} tool${t.inputMessageType ? ` (${t.inputMessageType})` : ""}.`),
|
|
39
|
-
timeout_ms: z.number().optional().describe("Per-call timeout in milliseconds."),
|
|
40
|
-
},
|
|
41
|
-
async execute(args) {
|
|
42
|
-
const result = await callGibsonTool(session.clients.component, {
|
|
43
|
-
name: t.name,
|
|
44
|
-
input: args.input,
|
|
45
|
-
...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}),
|
|
46
|
-
});
|
|
47
|
-
return formatToolResult(t.name, result);
|
|
48
|
-
},
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
/** The always-present dispatcher for tools the catalog did not surface. */
|
|
52
|
-
export function callToolTool(session) {
|
|
53
|
-
const z = tool.schema;
|
|
54
|
-
return tool({
|
|
55
|
-
description: "Invoke a Gibson tool by name through the harness. Use this when you know a " +
|
|
56
|
-
"tool's name but it is not in your tool list — Gibson tool discovery is not " +
|
|
57
|
-
"available on every daemon.",
|
|
58
|
-
args: {
|
|
59
|
-
name: z.string().describe("Registered Gibson tool name."),
|
|
60
|
-
input: z.record(z.string(), z.any()).describe("Input object matching the tool's input schema."),
|
|
61
|
-
timeout_ms: z.number().optional().describe("Per-call timeout in milliseconds."),
|
|
62
|
-
},
|
|
63
|
-
async execute(args) {
|
|
64
|
-
const result = await callGibsonTool(session.clients.component, {
|
|
65
|
-
name: args.name,
|
|
66
|
-
input: args.input,
|
|
67
|
-
...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}),
|
|
68
|
-
});
|
|
69
|
-
return formatToolResult(args.name, result);
|
|
70
|
-
},
|
|
71
|
-
});
|
|
72
|
-
}
|
|
73
|
-
/** List what Gibson exposes to this tenant — tools and enabled plugins. */
|
|
74
|
-
export function listToolsTool(session) {
|
|
75
|
-
return tool({
|
|
76
|
-
description: "List the Gibson tools and plugins available to this tenant, with the input " +
|
|
77
|
-
"message type each tool expects.",
|
|
78
|
-
args: {},
|
|
79
|
-
async execute() {
|
|
80
|
-
const [discovery, plugins] = await Promise.all([
|
|
81
|
-
listGibsonTools(session.clients.component),
|
|
82
|
-
listGibsonPlugins(session.clients.component).catch(() => []),
|
|
83
|
-
]);
|
|
84
|
-
const lines = [];
|
|
85
|
-
if (discovery.unavailable) {
|
|
86
|
-
lines.push(`Tool discovery is unavailable: ${discovery.unavailable}.`);
|
|
87
|
-
}
|
|
88
|
-
else if (discovery.tools.length === 0) {
|
|
89
|
-
lines.push("No Gibson tools are registered for this tenant.");
|
|
90
|
-
}
|
|
91
|
-
else {
|
|
92
|
-
lines.push("Gibson tools:");
|
|
93
|
-
for (const t of discovery.tools) {
|
|
94
|
-
lines.push(`- ${t.name} (${t.version}) — ${t.description || "no description"}`);
|
|
95
|
-
if (t.inputMessageType)
|
|
96
|
-
lines.push(` input: ${t.inputMessageType}`);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
const enabled = plugins.filter((p) => p.enabled);
|
|
100
|
-
if (enabled.length > 0) {
|
|
101
|
-
lines.push("", "Enabled plugins:");
|
|
102
|
-
for (const p of enabled) {
|
|
103
|
-
lines.push(`- ${p.name} (${p.version}) — methods: ${p.methods.join(", ") || "none"}`);
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
title: `${discovery.tools.length} tools, ${enabled.length} plugins`,
|
|
108
|
-
output: lines.join("\n"),
|
|
109
|
-
metadata: { tool_count: discovery.tools.length, plugin_count: enabled.length },
|
|
110
|
-
};
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
/** Invoke a method on an enabled Gibson plugin. */
|
|
115
|
-
export function queryPluginTool(session) {
|
|
116
|
-
const z = tool.schema;
|
|
117
|
-
return tool({
|
|
118
|
-
description: "Call a method on an enabled Gibson plugin through the harness. Use " +
|
|
119
|
-
"gibson_list_tools first to see which plugins and methods are available.",
|
|
120
|
-
args: {
|
|
121
|
-
plugin: z.string().describe("Registered plugin name."),
|
|
122
|
-
method: z.string().describe("Method to invoke on the plugin."),
|
|
123
|
-
params: z.record(z.string(), z.any()).optional().describe("Method parameters."),
|
|
124
|
-
timeout_ms: z.number().optional().describe("Per-call timeout in milliseconds."),
|
|
125
|
-
},
|
|
126
|
-
async execute(args) {
|
|
127
|
-
const result = await queryGibsonPlugin(session.clients.component, {
|
|
128
|
-
plugin: args.plugin,
|
|
129
|
-
method: args.method,
|
|
130
|
-
...(args.params ? { params: args.params } : {}),
|
|
131
|
-
...(args.timeout_ms ? { timeoutMs: args.timeout_ms } : {}),
|
|
132
|
-
});
|
|
133
|
-
return formatToolResult(`${args.plugin}.${args.method}`, result);
|
|
134
|
-
},
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
/**
|
|
138
|
-
* Discover the tenant's tools and build one opencode tool for each, plus the
|
|
139
|
-
* always-present dispatcher, catalog and plugin tools.
|
|
140
|
-
*
|
|
141
|
-
* Discovery runs once at plugin load. opencode reads the `tool` hook as a
|
|
142
|
-
* static object, so a tool registered later in the session would not be visible
|
|
143
|
-
* to the model anyway.
|
|
144
|
-
*/
|
|
145
|
-
export async function buildGibsonTools(session) {
|
|
146
|
-
const tools = {
|
|
147
|
-
gibson_call_tool: callToolTool(session),
|
|
148
|
-
gibson_list_tools: listToolsTool(session),
|
|
149
|
-
gibson_query_plugin: queryPluginTool(session),
|
|
150
|
-
};
|
|
151
|
-
const discovery = await listGibsonTools(session.clients.component);
|
|
152
|
-
for (const t of discovery.tools) {
|
|
153
|
-
// Never let a discovered tool shadow the built-ins.
|
|
154
|
-
const key = toolKey(t.name);
|
|
155
|
-
if (!(key in tools))
|
|
156
|
-
tools[key] = wrapGibsonTool(session, t);
|
|
157
|
-
}
|
|
158
|
-
return {
|
|
159
|
-
tools,
|
|
160
|
-
discovered: discovery.tools.length,
|
|
161
|
-
...(discovery.unavailable ? { note: discovery.unavailable } : {}),
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
/** Render a harness tool result, keeping an in-band tool error visible. */
|
|
165
|
-
function formatToolResult(name, result) {
|
|
166
|
-
if (result.error) {
|
|
167
|
-
return {
|
|
168
|
-
title: `${name} failed`,
|
|
169
|
-
output: `Gibson tool ${name} failed [${result.error.code}]: ${result.error.message}${result.error.retryable ? " (retryable)" : ""}`,
|
|
170
|
-
metadata: { error_code: result.error.code, retryable: result.error.retryable },
|
|
171
|
-
};
|
|
172
|
-
}
|
|
173
|
-
const text = typeof result.output === "string" ? result.output : JSON.stringify(result.output, null, 2);
|
|
174
|
-
return { title: name, output: text ?? "(no output)" };
|
|
175
|
-
}
|