@zeroroot-ai/gibson-mcp 0.1.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/LICENSE +21 -0
- package/README.md +160 -0
- package/dist/ambient.d.ts +7 -0
- package/dist/ambient.js +18 -0
- package/dist/ask.d.ts +70 -0
- package/dist/ask.js +111 -0
- package/dist/build.d.ts +83 -0
- package/dist/build.js +209 -0
- package/dist/cli.d.ts +88 -0
- package/dist/cli.js +186 -0
- package/dist/config.d.ts +45 -0
- package/dist/config.js +54 -0
- package/dist/discovery.d.ts +57 -0
- package/dist/discovery.js +132 -0
- package/dist/flags.d.ts +32 -0
- package/dist/flags.js +85 -0
- package/dist/generated/tools.d.ts +15 -0
- package/dist/generated/tools.js +276 -0
- package/dist/helpers/componentize.d.ts +19 -0
- package/dist/helpers/componentize.js +106 -0
- package/dist/helpers/context.d.ts +19 -0
- package/dist/helpers/context.js +19 -0
- package/dist/helpers/coverage.d.ts +23 -0
- package/dist/helpers/coverage.js +119 -0
- package/dist/helpers/delegate.d.ts +4 -0
- package/dist/helpers/delegate.js +182 -0
- package/dist/helpers/findings.d.ts +24 -0
- package/dist/helpers/findings.js +118 -0
- package/dist/helpers/index.d.ts +17 -0
- package/dist/helpers/index.js +24 -0
- package/dist/helpers/knowledge.d.ts +16 -0
- package/dist/helpers/knowledge.js +161 -0
- package/dist/helpers/tools.d.ts +113 -0
- package/dist/helpers/tools.js +80 -0
- package/dist/http.d.ts +57 -0
- package/dist/http.js +137 -0
- package/dist/inbox.d.ts +88 -0
- package/dist/inbox.js +176 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +25 -0
- package/dist/log.d.ts +4 -0
- package/dist/log.js +5 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +61 -0
- package/dist/mode.d.ts +32 -0
- package/dist/mode.js +21 -0
- package/dist/registry.d.ts +83 -0
- package/dist/registry.js +133 -0
- package/dist/resources.d.ts +63 -0
- package/dist/resources.js +98 -0
- package/dist/rpc.d.ts +80 -0
- package/dist/rpc.js +184 -0
- package/dist/schema.d.ts +31 -0
- package/dist/schema.js +143 -0
- package/dist/server.d.ts +22 -0
- package/dist/server.js +70 -0
- package/dist/session.d.ts +41 -0
- package/dist/session.js +170 -0
- package/dist/source.d.ts +30 -0
- package/dist/source.js +23 -0
- package/dist/state.d.ts +29 -0
- package/dist/state.js +37 -0
- package/dist/tls.d.ts +1 -0
- package/dist/tls.js +19 -0
- package/dist/tool.d.ts +17 -0
- package/dist/tool.js +22 -0
- package/dist/tools/connect.d.ts +24 -0
- package/dist/tools/connect.js +115 -0
- package/dist/tools/result.d.ts +7 -0
- package/dist/tools/result.js +16 -0
- package/dist/tools/status.d.ts +5 -0
- package/dist/tools/status.js +36 -0
- package/dist/turn.d.ts +75 -0
- package/dist/turn.js +95 -0
- package/package.json +58 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
+
/** The package version, read from package.json beside dist. */
|
|
5
|
+
export function packageVersion() {
|
|
6
|
+
try {
|
|
7
|
+
return createRequire(import.meta.url)("../package.json").version;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return "0.0.0";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export const SERVER_NAME = "gibson";
|
|
14
|
+
/**
|
|
15
|
+
* Bind one MCP protocol server to a transport. Every server reads the same
|
|
16
|
+
* registry: the stdio path has one, the HTTP path has one per session. A
|
|
17
|
+
* registry change reaches each attached server as `tools/list_changed`.
|
|
18
|
+
*/
|
|
19
|
+
export async function attachServer(binding, transport) {
|
|
20
|
+
const resources = binding.resources ?? [];
|
|
21
|
+
const prompts = binding.prompts ?? [];
|
|
22
|
+
const server = new Server({ name: SERVER_NAME, version: packageVersion() }, {
|
|
23
|
+
capabilities: {
|
|
24
|
+
tools: { listChanged: true },
|
|
25
|
+
...(resources.length > 0 ? { resources: {} } : {}),
|
|
26
|
+
...(prompts.length > 0 ? { prompts: {} } : {}),
|
|
27
|
+
},
|
|
28
|
+
...(binding.instructions ? { instructions: binding.instructions } : {}),
|
|
29
|
+
});
|
|
30
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
31
|
+
tools: binding.registry.list().map((t) => ({
|
|
32
|
+
name: t.name,
|
|
33
|
+
description: t.description,
|
|
34
|
+
inputSchema: t.inputSchema,
|
|
35
|
+
...(t.annotations ? { annotations: t.annotations } : {}),
|
|
36
|
+
})),
|
|
37
|
+
}));
|
|
38
|
+
server.setRequestHandler(CallToolRequestSchema, async (req, extra) => binding.registry.call(req.params.name, req.params.arguments ?? {}, {
|
|
39
|
+
headers: extra.requestInfo?.headers,
|
|
40
|
+
signal: extra.signal,
|
|
41
|
+
}));
|
|
42
|
+
if (resources.length > 0) {
|
|
43
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
44
|
+
resources: resources.map((r) => ({ uri: r.uri, name: r.name, title: r.title, description: r.description, mimeType: r.mimeType })),
|
|
45
|
+
}));
|
|
46
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
47
|
+
const resource = resources.find((r) => r.uri === req.params.uri);
|
|
48
|
+
if (!resource)
|
|
49
|
+
throw new Error(`no resource ${req.params.uri}`);
|
|
50
|
+
return { contents: [await resource.read()] };
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (prompts.length > 0) {
|
|
54
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
55
|
+
prompts: prompts.map((p) => ({ name: p.name, title: p.title, description: p.description, arguments: p.arguments })),
|
|
56
|
+
}));
|
|
57
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
58
|
+
const prompt = prompts.find((p) => p.name === req.params.name);
|
|
59
|
+
if (!prompt)
|
|
60
|
+
throw new Error(`no prompt ${req.params.name}`);
|
|
61
|
+
return prompt.get(req.params.arguments ?? {});
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const off = binding.registry.onChange(() => {
|
|
65
|
+
void server.sendToolListChanged().catch(() => { });
|
|
66
|
+
});
|
|
67
|
+
server.onclose = () => off();
|
|
68
|
+
await server.connect(transport);
|
|
69
|
+
return server;
|
|
70
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { connectGibson, openTaskHarness, startLiveMission, type GibsonSession, type KnowledgeSource, type LiveMission, type MissionOriginator } from "@zeroroot-ai/sdk";
|
|
2
|
+
import type { Settings } from "./config.js";
|
|
3
|
+
import { type Log } from "./log.js";
|
|
4
|
+
import { type Mode } from "./mode.js";
|
|
5
|
+
import { type CheckInSource } from "./source.js";
|
|
6
|
+
export { DEFAULT_AGENT_NAME } from "./config.js";
|
|
7
|
+
/** Everything the tools need, decided once per connect. */
|
|
8
|
+
export interface Gibson {
|
|
9
|
+
source: CheckInSource;
|
|
10
|
+
mode: Mode;
|
|
11
|
+
reason: string;
|
|
12
|
+
agentName: string;
|
|
13
|
+
hostKeyPath: string;
|
|
14
|
+
settings: Settings;
|
|
15
|
+
session?: GibsonSession;
|
|
16
|
+
live?: LiveMission;
|
|
17
|
+
knowledge?: KnowledgeSource;
|
|
18
|
+
/** The mission run this dispatched process belongs to, when the launch named one. */
|
|
19
|
+
runId?: string;
|
|
20
|
+
/** Release the mission and the component registration. Idempotent. */
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface OpenGibsonOptions {
|
|
24
|
+
settings: Settings;
|
|
25
|
+
log: Log;
|
|
26
|
+
/** The process environment, for the dispatched-grant contract. */
|
|
27
|
+
env?: NodeJS.ProcessEnv;
|
|
28
|
+
/** Test seams. */
|
|
29
|
+
connect?: typeof connectGibson;
|
|
30
|
+
start?: typeof startLiveMission;
|
|
31
|
+
harness?: typeof openTaskHarness;
|
|
32
|
+
originate?: MissionOriginator;
|
|
33
|
+
hostKeyExists?: (path: string) => boolean;
|
|
34
|
+
trust?: (path: string) => Promise<void>;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Pick the check-in source, check in, then make the session a live mission.
|
|
38
|
+
* Fails open at every step (ADR-0005): a platform failure lands one posture
|
|
39
|
+
* down, never in a refusal to start.
|
|
40
|
+
*/
|
|
41
|
+
export declare function openGibson(opts: OpenGibsonOptions): Promise<Gibson>;
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { componentKnowledge, connectGibson, openTaskHarness, SANDBOX_ENV, startLiveMission, taskKnowledge, } from "@zeroroot-ai/sdk";
|
|
3
|
+
import { submitMission } from "./cli.js";
|
|
4
|
+
import { TAG } from "./log.js";
|
|
5
|
+
import { decideMode } from "./mode.js";
|
|
6
|
+
import { decideSource } from "./source.js";
|
|
7
|
+
import { trustCA } from "./tls.js";
|
|
8
|
+
export { DEFAULT_AGENT_NAME } from "./config.js";
|
|
9
|
+
/**
|
|
10
|
+
* Pick the check-in source, check in, then make the session a live mission.
|
|
11
|
+
* Fails open at every step (ADR-0005): a platform failure lands one posture
|
|
12
|
+
* down, never in a refusal to start.
|
|
13
|
+
*/
|
|
14
|
+
export async function openGibson(opts) {
|
|
15
|
+
const { settings, log } = opts;
|
|
16
|
+
const env = opts.env ?? {};
|
|
17
|
+
const { hostKeyPath, agentName } = settings;
|
|
18
|
+
const hostKeyExists = (opts.hostKeyExists ?? existsSync)(hostKeyPath);
|
|
19
|
+
const decision = decideSource({
|
|
20
|
+
grant: env[SANDBOX_ENV.grant],
|
|
21
|
+
callbackEndpoint: env[SANDBOX_ENV.callbackEndpoint],
|
|
22
|
+
bootstrapToken: settings.bootstrapToken,
|
|
23
|
+
hostKeyExists,
|
|
24
|
+
});
|
|
25
|
+
for (const note of decision.notes)
|
|
26
|
+
log(`${TAG} check-in: ${note}`);
|
|
27
|
+
if (decision.source === "dispatched")
|
|
28
|
+
return openDispatched(env, settings, log, opts);
|
|
29
|
+
const source = decision.source;
|
|
30
|
+
const mode = decideMode({ platformURL: settings.platformURL, bootstrapToken: settings.bootstrapToken, hostKeyExists, targetId: settings.targetId }, hostKeyPath);
|
|
31
|
+
const standalone = (reason) => ({ source, mode: "standalone", reason, agentName, hostKeyPath, settings, close: async () => { } });
|
|
32
|
+
if (mode.mode === "standalone") {
|
|
33
|
+
log(`${TAG} ${source === "none" ? "not checked in" : "standalone"}: ${mode.reason}`);
|
|
34
|
+
return standalone(mode.reason);
|
|
35
|
+
}
|
|
36
|
+
if (settings.caCertPath) {
|
|
37
|
+
try {
|
|
38
|
+
await (opts.trust ?? trustCA)(settings.caCertPath);
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
log(`${TAG} CA at ${settings.caCertPath} not loaded: ${e.message}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// The bootstrap token is passed ONLY when no host key exists yet. Replaying a
|
|
45
|
+
// one-time token on every start would be rejected (gibson
|
|
46
|
+
// capabilitygrant_register.go:134-155).
|
|
47
|
+
const bootstrapToken = source === "bootstrap" ? settings.bootstrapToken : undefined;
|
|
48
|
+
let session;
|
|
49
|
+
try {
|
|
50
|
+
session = await (opts.connect ?? connectGibson)({
|
|
51
|
+
platformURL: settings.platformURL,
|
|
52
|
+
daemonURL: settings.daemonURL,
|
|
53
|
+
bootstrapToken,
|
|
54
|
+
hostKeyPath,
|
|
55
|
+
agentName,
|
|
56
|
+
agentMode: "interactive",
|
|
57
|
+
agent: { name: agentName, version: "0.0.0", capabilities: ["code"] },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (e) {
|
|
61
|
+
const reason = `Gibson connect failed: ${e.message}`;
|
|
62
|
+
log(`${TAG} ${reason}; continuing standalone`);
|
|
63
|
+
return standalone(reason);
|
|
64
|
+
}
|
|
65
|
+
log(`${TAG} checked in via ${source === "bootstrap" ? "the bootstrap token (first check-in)" : "the registered host key"} as ${agentName} (component_scope=${session.componentScope})`);
|
|
66
|
+
if (source === "bootstrap")
|
|
67
|
+
log(`${TAG} host key written to ${hostKeyPath}; the bootstrap token is spent`);
|
|
68
|
+
const component = {
|
|
69
|
+
source,
|
|
70
|
+
mode: "component",
|
|
71
|
+
reason: mode.reason,
|
|
72
|
+
agentName,
|
|
73
|
+
hostKeyPath,
|
|
74
|
+
settings,
|
|
75
|
+
session,
|
|
76
|
+
knowledge: componentKnowledge(session.clients.component),
|
|
77
|
+
close: async () => session.stop(),
|
|
78
|
+
};
|
|
79
|
+
if (mode.mode === "component") {
|
|
80
|
+
log(`${TAG} component posture: ${mode.reason}`);
|
|
81
|
+
return component;
|
|
82
|
+
}
|
|
83
|
+
let live;
|
|
84
|
+
try {
|
|
85
|
+
live = await (opts.start ?? startLiveMission)(session, {
|
|
86
|
+
agentName,
|
|
87
|
+
targetId: settings.targetId,
|
|
88
|
+
// The person originates the session mission through the CLI login
|
|
89
|
+
// session; the component only claims its dispatch (gibson ADR-0063,
|
|
90
|
+
// ADR-0007 decision 3: one live mission per session).
|
|
91
|
+
originate: opts.originate ?? ((definition, targetId) => submitMission(definition, targetId, { env, gibsonURL: settings.platformURL, tenant: settings.tenant })),
|
|
92
|
+
harness: { insecure: settings.callbackInsecure },
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
const reason = `live mission failed: ${e.message}`;
|
|
97
|
+
log(`${TAG} ${reason}; continuing as a component`);
|
|
98
|
+
return { ...component, reason };
|
|
99
|
+
}
|
|
100
|
+
log(`${TAG} live mission ${live.missionId} (work ${live.workId}); reads and writes use the task grant`);
|
|
101
|
+
let closed = false;
|
|
102
|
+
return {
|
|
103
|
+
source,
|
|
104
|
+
mode: "live",
|
|
105
|
+
reason: "",
|
|
106
|
+
agentName,
|
|
107
|
+
hostKeyPath,
|
|
108
|
+
settings,
|
|
109
|
+
session,
|
|
110
|
+
live,
|
|
111
|
+
knowledge: taskKnowledge(live.harness),
|
|
112
|
+
close: async () => {
|
|
113
|
+
if (closed)
|
|
114
|
+
return;
|
|
115
|
+
closed = true;
|
|
116
|
+
try {
|
|
117
|
+
await live.end({ output: { ended: "session closed" } });
|
|
118
|
+
}
|
|
119
|
+
finally {
|
|
120
|
+
session.stop();
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* The dispatched source: the daemon launched this process with a grant and
|
|
127
|
+
* a callback endpoint. The launch already decided everything; the server
|
|
128
|
+
* opens the task harness from the grant and serves the tools that run under
|
|
129
|
+
* it. No host key, no check-in, no mission creation, no state file.
|
|
130
|
+
*
|
|
131
|
+
* A grant the harness cannot open still fails open: the process serves
|
|
132
|
+
* standalone with the reason in `gibson_status`, so the run's log says why
|
|
133
|
+
* the tools are missing instead of the sandbox dying with no tool surface.
|
|
134
|
+
*/
|
|
135
|
+
async function openDispatched(env, settings, log, opts) {
|
|
136
|
+
const endpoint = env[SANDBOX_ENV.callbackEndpoint];
|
|
137
|
+
const token = env[SANDBOX_ENV.grant];
|
|
138
|
+
const runId = env[SANDBOX_ENV.missionRunId] || undefined;
|
|
139
|
+
if (settings.caCertPath) {
|
|
140
|
+
await (opts.trust ?? trustCA)(settings.caCertPath).catch((e) => log(`${TAG} CA not loaded: ${e.message}`));
|
|
141
|
+
}
|
|
142
|
+
const base = { source: "dispatched", hostKeyPath: settings.hostKeyPath, settings, ...(runId ? { runId } : {}) };
|
|
143
|
+
let harness;
|
|
144
|
+
try {
|
|
145
|
+
harness = (opts.harness ?? openTaskHarness)({ endpoint, token, insecure: settings.callbackInsecure });
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
const reason = `dispatched grant unusable: ${e.message}`;
|
|
149
|
+
log(`${TAG} ${reason}; continuing standalone`);
|
|
150
|
+
return { ...base, mode: "standalone", reason, agentName: settings.agentName, close: async () => { } };
|
|
151
|
+
}
|
|
152
|
+
const missionId = harness.context.missionId;
|
|
153
|
+
const live = {
|
|
154
|
+
missionId,
|
|
155
|
+
workId: runId ?? harness.context.taskId,
|
|
156
|
+
harness,
|
|
157
|
+
// The dispatched process reports completion by exiting; the launcher owns the node.
|
|
158
|
+
end: async () => harness.stop(),
|
|
159
|
+
};
|
|
160
|
+
log(`${TAG} dispatched: mission ${missionId}${runId ? ` run ${runId}` : ""} as ${harness.context.agentName}; reads and writes use the dispatch grant`);
|
|
161
|
+
return {
|
|
162
|
+
...base,
|
|
163
|
+
mode: "task",
|
|
164
|
+
reason: "",
|
|
165
|
+
agentName: harness.context.agentName,
|
|
166
|
+
live,
|
|
167
|
+
knowledge: taskKnowledge(harness),
|
|
168
|
+
close: async () => harness.stop(),
|
|
169
|
+
};
|
|
170
|
+
}
|
package/dist/source.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The check-in source: how this server gets its credential (gibson#1706,
|
|
3
|
+
* decision 4). Three, chosen by what is present at start, never mixed.
|
|
4
|
+
*
|
|
5
|
+
* - `dispatched`: the daemon launched the process. `GIBSON_CG_JWT` and
|
|
6
|
+
* `GIBSON_CALLBACK_ENDPOINT` are the only credential. The server joins
|
|
7
|
+
* the run it was launched for: no enrollment, no state file, no mission.
|
|
8
|
+
* - `bootstrap`: a person minted a one-time bootstrap token earlier and this
|
|
9
|
+
* host has no key yet. The server checks in unattended with it once, and
|
|
10
|
+
* the host key carries every later start.
|
|
11
|
+
* - `enrolled`: the host key exists. The server checks in with it.
|
|
12
|
+
* - `none`: nothing is present. The server exposes `gibson_login` and
|
|
13
|
+
* `gibson_connect`, the device flow through the `gibson` CLI, and a person
|
|
14
|
+
* enrolls the host in the session.
|
|
15
|
+
*
|
|
16
|
+
* Priority: dispatched wins. The server never mints identity (ADR-0045).
|
|
17
|
+
*/
|
|
18
|
+
export type CheckInSource = "dispatched" | "bootstrap" | "enrolled" | "none";
|
|
19
|
+
export interface SourceInputs {
|
|
20
|
+
grant?: string;
|
|
21
|
+
callbackEndpoint?: string;
|
|
22
|
+
bootstrapToken?: string;
|
|
23
|
+
hostKeyExists: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface SourceDecision {
|
|
26
|
+
source: CheckInSource;
|
|
27
|
+
/** What was present but ignored, one line each, for the log. */
|
|
28
|
+
notes: string[];
|
|
29
|
+
}
|
|
30
|
+
export declare function decideSource(i: SourceInputs): SourceDecision;
|
package/dist/source.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export function decideSource(i) {
|
|
2
|
+
const notes = [];
|
|
3
|
+
if (i.grant && i.callbackEndpoint) {
|
|
4
|
+
if (i.hostKeyExists)
|
|
5
|
+
notes.push("a host key exists, but the dispatched grant wins; the host key is ignored");
|
|
6
|
+
if (i.bootstrapToken)
|
|
7
|
+
notes.push("GIBSON_BOOTSTRAP_TOKEN is set, but the dispatched grant wins; the token is ignored");
|
|
8
|
+
return { source: "dispatched", notes };
|
|
9
|
+
}
|
|
10
|
+
if (i.grant)
|
|
11
|
+
notes.push("GIBSON_CG_JWT is set without GIBSON_CALLBACK_ENDPOINT; the grant is ignored");
|
|
12
|
+
if (i.callbackEndpoint)
|
|
13
|
+
notes.push("GIBSON_CALLBACK_ENDPOINT is set without GIBSON_CG_JWT; the endpoint is ignored");
|
|
14
|
+
if (i.hostKeyExists) {
|
|
15
|
+
if (i.bootstrapToken) {
|
|
16
|
+
notes.push("GIBSON_BOOTSTRAP_TOKEN is set, but this host has a key; the token is ignored (a one-time token cannot be replayed)");
|
|
17
|
+
}
|
|
18
|
+
return { source: "enrolled", notes };
|
|
19
|
+
}
|
|
20
|
+
if (i.bootstrapToken)
|
|
21
|
+
return { source: "bootstrap", notes };
|
|
22
|
+
return { source: "none", notes };
|
|
23
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The handoff between the MCP server and a host's hook processes.
|
|
3
|
+
*
|
|
4
|
+
* A hook runs as a separate process and cannot reach the server's session.
|
|
5
|
+
* The server writes one small file per working directory: the ambient
|
|
6
|
+
* knowledge block for a session-start hook, and the live-mission coordinates
|
|
7
|
+
* for a session-end hook. The files live in the state directory beside the
|
|
8
|
+
* host key. The task grant in the live file is bounded (30 minutes, renewed
|
|
9
|
+
* by the server), and a hook that finds a stale one fails open.
|
|
10
|
+
*
|
|
11
|
+
* The directory is `~/.zerocool/`, not a per-host subdirectory: the server
|
|
12
|
+
* is host-agnostic. A dispatched run writes no state file at all.
|
|
13
|
+
*/
|
|
14
|
+
export interface LiveState {
|
|
15
|
+
missionId: string;
|
|
16
|
+
workId: string;
|
|
17
|
+
endpoint: string;
|
|
18
|
+
token: string;
|
|
19
|
+
insecure: boolean;
|
|
20
|
+
/** Unix ms. */
|
|
21
|
+
writtenAt: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function stateDir(env?: NodeJS.ProcessEnv): string;
|
|
24
|
+
export declare function keyFor(cwd: string): string;
|
|
25
|
+
export declare function writeAmbient(dir: string, cwd: string, block: string): Promise<void>;
|
|
26
|
+
export declare function readAmbient(dir: string, cwd: string): Promise<string>;
|
|
27
|
+
export declare function writeLive(dir: string, cwd: string, state: LiveState): Promise<void>;
|
|
28
|
+
export declare function readLive(dir: string, cwd: string): Promise<LiveState | undefined>;
|
|
29
|
+
export declare function clearLive(dir: string, cwd: string): Promise<void>;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile, rm } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
export function stateDir(env = process.env) {
|
|
6
|
+
return env.ZEROCOOL_STATE_DIR ?? join(homedir(), ".zerocool");
|
|
7
|
+
}
|
|
8
|
+
export function keyFor(cwd) {
|
|
9
|
+
return createHash("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
10
|
+
}
|
|
11
|
+
export async function writeAmbient(dir, cwd, block) {
|
|
12
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
13
|
+
await writeFile(join(dir, `ambient-${keyFor(cwd)}.md`), block, { encoding: "utf8", mode: 0o600 });
|
|
14
|
+
}
|
|
15
|
+
export async function readAmbient(dir, cwd) {
|
|
16
|
+
try {
|
|
17
|
+
return await readFile(join(dir, `ambient-${keyFor(cwd)}.md`), "utf8");
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return "";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function writeLive(dir, cwd, state) {
|
|
24
|
+
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
25
|
+
await writeFile(join(dir, `live-${keyFor(cwd)}.json`), JSON.stringify(state), { encoding: "utf8", mode: 0o600 });
|
|
26
|
+
}
|
|
27
|
+
export async function readLive(dir, cwd) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(await readFile(join(dir, `live-${keyFor(cwd)}.json`), "utf8"));
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
export async function clearLive(dir, cwd) {
|
|
36
|
+
await rm(join(dir, `live-${keyFor(cwd)}.json`), { force: true });
|
|
37
|
+
}
|
package/dist/tls.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function trustCA(path: string): Promise<void>;
|
package/dist/tls.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import tls from "node:tls";
|
|
3
|
+
/**
|
|
4
|
+
* Trust a private CA for every TLS client in this process, at runtime.
|
|
5
|
+
*
|
|
6
|
+
* A self-hosted Gibson (kind, on-prem) fronts its edge with its own CA. Node
|
|
7
|
+
* reads NODE_EXTRA_CA_CERTS only at start, and the connect happens mid-session,
|
|
8
|
+
* so the CA is added through `tls.setDefaultCACertificates` (Node 22.15+).
|
|
9
|
+
* Both `fetch` (the Capability Grant register call) and the gRPC transports
|
|
10
|
+
* use the default CA set, so one call covers all of them.
|
|
11
|
+
*/
|
|
12
|
+
const trusted = new Set();
|
|
13
|
+
export async function trustCA(path) {
|
|
14
|
+
if (trusted.has(path))
|
|
15
|
+
return;
|
|
16
|
+
const pem = await readFile(path, "utf8");
|
|
17
|
+
tls.setDefaultCACertificates([...tls.getCACertificates("default"), pem]);
|
|
18
|
+
trusted.add(path);
|
|
19
|
+
}
|
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { z, type ZodRawShape } from "zod";
|
|
3
|
+
import type { JsonSchema, ToolAnnotations, ToolContext, ToolDefinition } from "./registry.js";
|
|
4
|
+
/**
|
|
5
|
+
* A hand-signed tool: a zod shape for the input, a typed handler. The JSON
|
|
6
|
+
* Schema the host sees is derived from the shape, so a description written
|
|
7
|
+
* once on a field reaches the model.
|
|
8
|
+
*/
|
|
9
|
+
export interface ToolSpec<S extends ZodRawShape> {
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
input: S;
|
|
13
|
+
annotations?: ToolAnnotations;
|
|
14
|
+
handler: (args: z.infer<z.ZodObject<S>>, ctx: ToolContext) => Promise<CallToolResult>;
|
|
15
|
+
}
|
|
16
|
+
export declare function jsonSchemaOf(shape: ZodRawShape): JsonSchema;
|
|
17
|
+
export declare function defineTool<S extends ZodRawShape>(spec: ToolSpec<S>): ToolDefinition;
|
package/dist/tool.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { failure } from "./tools/result.js";
|
|
3
|
+
export function jsonSchemaOf(shape) {
|
|
4
|
+
const raw = z.toJSONSchema(z.object(shape), { io: "input" });
|
|
5
|
+
delete raw.$schema;
|
|
6
|
+
return { ...raw, type: "object" };
|
|
7
|
+
}
|
|
8
|
+
export function defineTool(spec) {
|
|
9
|
+
const schema = z.object(spec.input);
|
|
10
|
+
return {
|
|
11
|
+
name: spec.name,
|
|
12
|
+
description: spec.description,
|
|
13
|
+
inputSchema: jsonSchemaOf(spec.input),
|
|
14
|
+
...(spec.annotations ? { annotations: spec.annotations } : {}),
|
|
15
|
+
handler: async (raw, ctx) => {
|
|
16
|
+
const parsed = schema.safeParse(raw);
|
|
17
|
+
if (!parsed.success)
|
|
18
|
+
return failure(`${spec.name}: invalid arguments`, z.prettifyError(parsed.error));
|
|
19
|
+
return spec.handler(parsed.data, ctx);
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type CliOptions } from "../cli.js";
|
|
2
|
+
import type { ToolDefinition } from "../registry.js";
|
|
3
|
+
import { type Gibson, type OpenGibsonOptions } from "../session.js";
|
|
4
|
+
/**
|
|
5
|
+
* gibson_login and gibson_connect: the in-session path to a platform, for a
|
|
6
|
+
* host with no key and no token. `gibson_login` starts the CLI device flow
|
|
7
|
+
* and returns the URL and code. `gibson_connect` enrolls through the CLI
|
|
8
|
+
* session, trusts the private CA, checks in, picks the target, starts the
|
|
9
|
+
* live mission and swaps the tool set live. The settings it learns are
|
|
10
|
+
* persisted, so later sessions connect on their own.
|
|
11
|
+
*/
|
|
12
|
+
export interface ConnectDeps {
|
|
13
|
+
open?: Pick<OpenGibsonOptions, "connect" | "start" | "hostKeyExists" | "trust" | "harness">;
|
|
14
|
+
cli?: CliOptions;
|
|
15
|
+
}
|
|
16
|
+
export interface ConnectTarget {
|
|
17
|
+
current(): Gibson;
|
|
18
|
+
/** Replace the connection and re-register the posture's tools. */
|
|
19
|
+
upgrade(next: Gibson): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
export declare function loginTool(target: ConnectTarget, env: NodeJS.ProcessEnv, deps: ConnectDeps): ToolDefinition;
|
|
22
|
+
export declare function connectTool(target: ConnectTarget, env: NodeJS.ProcessEnv, cwd: string, deps: ConnectDeps): ToolDefinition;
|
|
23
|
+
/** What a coding workspace is, as a URL: its git origin, else the directory. */
|
|
24
|
+
export declare function workspaceURL(cwd: string): Promise<string>;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { createTarget, enrollIdentity, listTargets, startLogin } from "../cli.js";
|
|
6
|
+
import { loadSettings, writeConfig } from "../config.js";
|
|
7
|
+
import { log } from "../log.js";
|
|
8
|
+
import { openGibson } from "../session.js";
|
|
9
|
+
import { defineTool } from "../tool.js";
|
|
10
|
+
import { failure, text } from "./result.js";
|
|
11
|
+
import { describeGibson } from "./status.js";
|
|
12
|
+
export function loginTool(target, env, deps) {
|
|
13
|
+
return defineTool({
|
|
14
|
+
name: "gibson_login",
|
|
15
|
+
description: "Sign in to a Gibson platform as the person running this session. Starts the gibson CLI device " +
|
|
16
|
+
"flow and returns a URL and a code. The person opens the URL, confirms the code, and then calls " +
|
|
17
|
+
"gibson_connect. Needed once per host, and again when the login session expires.",
|
|
18
|
+
input: {
|
|
19
|
+
platform_url: z.string().optional().describe("Platform URL, e.g. https://api.example.com. Defaults to the last one used."),
|
|
20
|
+
},
|
|
21
|
+
handler: async (args) => {
|
|
22
|
+
const s = target.current().settings;
|
|
23
|
+
try {
|
|
24
|
+
const p = await startLogin({ ...deps.cli, env, gibsonURL: args.platform_url ?? s.platformURL });
|
|
25
|
+
return text("sign in", `Open ${p.url} and confirm the code ${p.code}. The CLI waits for the approval. When it is approved, call gibson_connect.`);
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return failure("gibson_login failed", e.message);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function connectTool(target, env, cwd, deps) {
|
|
34
|
+
const cli = (s) => ({ ...deps.cli, env, gibsonURL: s.platformURL, tenant: s.tenant });
|
|
35
|
+
return defineTool({
|
|
36
|
+
name: "gibson_connect",
|
|
37
|
+
description: "Connect this session to a Gibson platform without a restart: enroll this host through the gibson " +
|
|
38
|
+
"CLI login session if needed, check in, pick the target, start the live mission, and add the " +
|
|
39
|
+
"platform tools. The settings are kept, so later sessions connect on their own. Call gibson_status afterwards.",
|
|
40
|
+
input: {
|
|
41
|
+
platform_url: z.string().optional().describe("Platform URL. Defaults to the gibson CLI login session or the saved config."),
|
|
42
|
+
target_id: z.string().optional().describe("Target id for the live mission. Defaults to the only target in the tenant."),
|
|
43
|
+
create_target: z.string().optional().describe("Create a target with this name when none exists, and use it."),
|
|
44
|
+
target_url: z.string().optional().describe("URL of the target to create. Defaults to the workspace's git origin, or file://<cwd>."),
|
|
45
|
+
bootstrap_token: z.string().optional().describe("One-time enrollment token. Only when the CLI cannot mint one."),
|
|
46
|
+
ca_cert_path: z.string().optional().describe("Path to a private CA to trust. Defaults to the CLI session's CA."),
|
|
47
|
+
callback_insecure: z.boolean().optional().describe("Dial the callback endpoint without TLS. Local daemons only."),
|
|
48
|
+
},
|
|
49
|
+
handler: async (args) => {
|
|
50
|
+
const base = await loadSettings(env);
|
|
51
|
+
const s = {
|
|
52
|
+
...base,
|
|
53
|
+
platformURL: args.platform_url ?? base.platformURL,
|
|
54
|
+
targetId: args.target_id ?? base.targetId,
|
|
55
|
+
caCertPath: args.ca_cert_path ?? base.caCertPath,
|
|
56
|
+
callbackInsecure: args.callback_insecure ?? base.callbackInsecure,
|
|
57
|
+
bootstrapToken: args.bootstrap_token ?? base.bootstrapToken,
|
|
58
|
+
};
|
|
59
|
+
if (!s.platformURL) {
|
|
60
|
+
return failure("no platform", "Pass platform_url, or call gibson_login first so the CLI session names the platform.");
|
|
61
|
+
}
|
|
62
|
+
const hostKeyExists = (deps.open?.hostKeyExists ?? existsSync)(s.hostKeyPath);
|
|
63
|
+
try {
|
|
64
|
+
if (!hostKeyExists && !s.bootstrapToken) {
|
|
65
|
+
s.bootstrapToken = await enrollIdentity(s.agentName, cli(s));
|
|
66
|
+
}
|
|
67
|
+
if (!s.targetId) {
|
|
68
|
+
if (args.create_target) {
|
|
69
|
+
s.targetId = await createTarget(args.create_target, args.target_url ?? (await workspaceURL(cwd)), cli(s));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const targets = await listTargets(cli(s));
|
|
73
|
+
if (targets.length === 1)
|
|
74
|
+
s.targetId = targets[0].id;
|
|
75
|
+
else if (targets.length === 0) {
|
|
76
|
+
return failure("no target", "The tenant has no target. Call gibson_connect again with create_target set to a name for this workspace.");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
return failure("choose a target", `The tenant has ${targets.length} targets. Call gibson_connect again with target_id:\n${targets.map((t) => `- ${t.id} ${t.name} (${t.type}, ${t.status})`).join("\n")}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (e) {
|
|
85
|
+
return failure("gibson_connect failed", e.message);
|
|
86
|
+
}
|
|
87
|
+
await writeConfig(env, {
|
|
88
|
+
platformURL: s.platformURL,
|
|
89
|
+
targetId: s.targetId,
|
|
90
|
+
...(s.caCertPath ? { caCertPath: s.caCertPath } : {}),
|
|
91
|
+
...(s.daemonURL ? { daemonURL: s.daemonURL } : {}),
|
|
92
|
+
callbackInsecure: s.callbackInsecure,
|
|
93
|
+
});
|
|
94
|
+
const previous = target.current();
|
|
95
|
+
await previous.close().catch(() => { });
|
|
96
|
+
const next = await openGibson({ settings: s, log, env, ...deps.open });
|
|
97
|
+
await target.upgrade(next);
|
|
98
|
+
const body = describeGibson(next);
|
|
99
|
+
return next.mode === "live" ? text("connected", body) : failure("connected, but not live", body);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
/** What a coding workspace is, as a URL: its git origin, else the directory. */
|
|
104
|
+
export async function workspaceURL(cwd) {
|
|
105
|
+
try {
|
|
106
|
+
const { stdout } = await promisify(execFile)("git", ["-C", cwd, "remote", "get-url", "origin"]);
|
|
107
|
+
const remote = stdout.trim();
|
|
108
|
+
if (remote)
|
|
109
|
+
return remote;
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// not a git checkout, or no origin
|
|
113
|
+
}
|
|
114
|
+
return `file://${cwd}`;
|
|
115
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
/** One text block. MCP has no title/metadata split, so the title leads the text. */
|
|
3
|
+
export declare function text(title: string, body: string, isError?: boolean): CallToolResult;
|
|
4
|
+
/** Error text for a failure the model can act on. */
|
|
5
|
+
export declare function failure(title: string, body: string): CallToolResult;
|
|
6
|
+
/** A JSON document as the one text block, plus the structured copy. */
|
|
7
|
+
export declare function json(value: unknown): CallToolResult;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** One text block. MCP has no title/metadata split, so the title leads the text. */
|
|
2
|
+
export function text(title, body, isError = false) {
|
|
3
|
+
return { content: [{ type: "text", text: title ? `${title}\n\n${body}` : body }], ...(isError ? { isError } : {}) };
|
|
4
|
+
}
|
|
5
|
+
/** Error text for a failure the model can act on. */
|
|
6
|
+
export function failure(title, body) {
|
|
7
|
+
return text(title, body, true);
|
|
8
|
+
}
|
|
9
|
+
/** A JSON document as the one text block, plus the structured copy. */
|
|
10
|
+
export function json(value) {
|
|
11
|
+
const body = JSON.stringify(value, null, 2);
|
|
12
|
+
return {
|
|
13
|
+
content: [{ type: "text", text: body }],
|
|
14
|
+
...(value !== null && typeof value === "object" && !Array.isArray(value) ? { structuredContent: value } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ToolDefinition } from "../registry.js";
|
|
2
|
+
import type { Gibson } from "../session.js";
|
|
3
|
+
/** One tool that says how this session is connected to Gibson and why. */
|
|
4
|
+
export declare function statusTool(current: () => Gibson): ToolDefinition;
|
|
5
|
+
export declare function describeGibson(g: Gibson): string;
|