@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +160 -0
  3. package/dist/ambient.d.ts +7 -0
  4. package/dist/ambient.js +18 -0
  5. package/dist/ask.d.ts +70 -0
  6. package/dist/ask.js +111 -0
  7. package/dist/build.d.ts +83 -0
  8. package/dist/build.js +209 -0
  9. package/dist/cli.d.ts +88 -0
  10. package/dist/cli.js +186 -0
  11. package/dist/config.d.ts +45 -0
  12. package/dist/config.js +54 -0
  13. package/dist/discovery.d.ts +57 -0
  14. package/dist/discovery.js +132 -0
  15. package/dist/flags.d.ts +32 -0
  16. package/dist/flags.js +85 -0
  17. package/dist/generated/tools.d.ts +15 -0
  18. package/dist/generated/tools.js +276 -0
  19. package/dist/helpers/componentize.d.ts +19 -0
  20. package/dist/helpers/componentize.js +106 -0
  21. package/dist/helpers/context.d.ts +19 -0
  22. package/dist/helpers/context.js +19 -0
  23. package/dist/helpers/coverage.d.ts +23 -0
  24. package/dist/helpers/coverage.js +119 -0
  25. package/dist/helpers/delegate.d.ts +4 -0
  26. package/dist/helpers/delegate.js +182 -0
  27. package/dist/helpers/findings.d.ts +24 -0
  28. package/dist/helpers/findings.js +118 -0
  29. package/dist/helpers/index.d.ts +17 -0
  30. package/dist/helpers/index.js +24 -0
  31. package/dist/helpers/knowledge.d.ts +16 -0
  32. package/dist/helpers/knowledge.js +161 -0
  33. package/dist/helpers/tools.d.ts +113 -0
  34. package/dist/helpers/tools.js +80 -0
  35. package/dist/http.d.ts +57 -0
  36. package/dist/http.js +137 -0
  37. package/dist/inbox.d.ts +88 -0
  38. package/dist/inbox.js +176 -0
  39. package/dist/index.d.ts +23 -0
  40. package/dist/index.js +25 -0
  41. package/dist/log.d.ts +4 -0
  42. package/dist/log.js +5 -0
  43. package/dist/main.d.ts +2 -0
  44. package/dist/main.js +61 -0
  45. package/dist/mode.d.ts +32 -0
  46. package/dist/mode.js +21 -0
  47. package/dist/registry.d.ts +83 -0
  48. package/dist/registry.js +133 -0
  49. package/dist/resources.d.ts +63 -0
  50. package/dist/resources.js +98 -0
  51. package/dist/rpc.d.ts +80 -0
  52. package/dist/rpc.js +184 -0
  53. package/dist/schema.d.ts +31 -0
  54. package/dist/schema.js +143 -0
  55. package/dist/server.d.ts +22 -0
  56. package/dist/server.js +70 -0
  57. package/dist/session.d.ts +41 -0
  58. package/dist/session.js +170 -0
  59. package/dist/source.d.ts +30 -0
  60. package/dist/source.js +23 -0
  61. package/dist/state.d.ts +29 -0
  62. package/dist/state.js +37 -0
  63. package/dist/tls.d.ts +1 -0
  64. package/dist/tls.js +19 -0
  65. package/dist/tool.d.ts +17 -0
  66. package/dist/tool.js +22 -0
  67. package/dist/tools/connect.d.ts +24 -0
  68. package/dist/tools/connect.js +115 -0
  69. package/dist/tools/result.d.ts +7 -0
  70. package/dist/tools/result.js +16 -0
  71. package/dist/tools/status.d.ts +5 -0
  72. package/dist/tools/status.js +36 -0
  73. package/dist/turn.d.ts +75 -0
  74. package/dist/turn.js +95 -0
  75. package/package.json +58 -0
package/dist/build.js ADDED
@@ -0,0 +1,209 @@
1
+ import { loadSettings } from "./config.js";
2
+ import { DEFAULT_STREAM_LIMIT } from "./flags.js";
3
+ import { log, TAG } from "./log.js";
4
+ import { ToolRegistry } from "./registry.js";
5
+ import { attachServer } from "./server.js";
6
+ import { AnswerRouter, askTool } from "./ask.js";
7
+ import { startDiscovery } from "./discovery.js";
8
+ import { inboxAvailable, openInbox, routeAnswers } from "./inbox.js";
9
+ import { helperToolsFor } from "./helpers/index.js";
10
+ import { rpcTools } from "./rpc.js";
11
+ import { createTurnController, TURN_GRANT_HEADER } from "./turn.js";
12
+ import { ambientPrompt, ambientSource, resources } from "./resources.js";
13
+ import { openGibson } from "./session.js";
14
+ import { clearLive, stateDir, writeAmbient, writeLive } from "./state.js";
15
+ import { connectTool, loginTool } from "./tools/connect.js";
16
+ import { statusTool } from "./tools/status.js";
17
+ export async function buildSurface(env, cwd, deps = {}) {
18
+ const settings = await loadSettings(env);
19
+ let gibson = await openGibson({ settings, log, env, ...deps.open });
20
+ const registry = new ToolRegistry();
21
+ const posture = registry.group();
22
+ const streamLimit = deps.streamLimit ?? DEFAULT_STREAM_LIMIT;
23
+ let discovery;
24
+ let closed = false;
25
+ // A posture that holds a task grant can serve turns: the grant it started
26
+ // with is the base, and a turn's grant replaces it for that turn's calls.
27
+ let turns;
28
+ let inbox;
29
+ const answers = new AnswerRouter();
30
+ let ambient = ambientSource(gibson, env);
31
+ if (gibson.live) {
32
+ turns = createTurnController({ base: gibson.live.harness, insecure: gibson.settings.callbackInsecure, log });
33
+ registry.use((ctx, next) => {
34
+ const raw = ctx.headers?.[TURN_GRANT_HEADER];
35
+ const grant = Array.isArray(raw) ? raw[0] : raw;
36
+ return turns.withGrant(grant, next);
37
+ });
38
+ }
39
+ /**
40
+ * The inbox is a lifetime RPC: it runs under the base grant, on the
41
+ * transport the launch opened, never under a turn's. It exists only where
42
+ * the daemon carries the inbox RPCs, so a member sandbox has an `ask` tool
43
+ * and an ordinary dispatched run does not.
44
+ */
45
+ if (gibson.live && inboxAvailable(gibson.live.harness)) {
46
+ inbox = routeAnswers(openInbox({ harness: gibson.live.harness, log }), answers, log);
47
+ }
48
+ const postureCtx = () => ({
49
+ env,
50
+ cwd,
51
+ streamLimit,
52
+ ...(deps.discoveryIntervalMs === undefined ? {} : { discoveryIntervalMs: deps.discoveryIntervalMs }),
53
+ ...(turns ? { taskTransport: turns.transport() } : {}),
54
+ ambient: { block: (q) => ambient.block(q) },
55
+ ...(inbox
56
+ ? {
57
+ ask: askTool({ jobId: () => turns?.current()?.jobId, inbox, answers, log }),
58
+ }
59
+ : {}),
60
+ });
61
+ const surface = {
62
+ registry,
63
+ current: () => gibson,
64
+ upgrade: async (next) => {
65
+ discovery?.stop();
66
+ discovery = undefined;
67
+ await registry.batchAsync(async () => {
68
+ posture.clear();
69
+ gibson = next;
70
+ // The block belongs to the connection it was read over. Keeping the
71
+ // old one after a connect would hand the model another tenant's
72
+ // context.
73
+ ambient = ambientSource(next, env);
74
+ discovery = await registerPosture(posture, next, postureCtx());
75
+ });
76
+ },
77
+ attach: (transport) => attachServer({
78
+ registry,
79
+ instructions: instructions(),
80
+ resources: resources(() => gibson, { block: (q) => ambient.block(q) }),
81
+ prompts: [ambientPrompt({ block: (q) => ambient.block(q) })],
82
+ }, transport),
83
+ health: () => ({ source: gibson.source, posture: gibson.mode, tools: registry.size(), ...(turns?.current() ? { job: turns.current().jobId } : {}) }),
84
+ ...(inbox ? { inbox } : {}),
85
+ ...(turns
86
+ ? {
87
+ turn: {
88
+ set: (t) => turns.set({ jobId: t.jobId, grant: t.grant, endpoint: t.endpoint ?? gibson.live.harness.endpoint, insecure: t.insecure ?? gibson.settings.callbackInsecure }),
89
+ clear: () => turns.clear(),
90
+ current: () => {
91
+ const c = turns.current();
92
+ return c ? { jobId: c.jobId, endpoint: c.endpoint } : undefined;
93
+ },
94
+ },
95
+ }
96
+ : {}),
97
+ close: async () => {
98
+ if (closed)
99
+ return;
100
+ closed = true;
101
+ discovery?.stop();
102
+ inbox?.stop();
103
+ turns?.close();
104
+ if (gibson.source !== "dispatched")
105
+ await clearLive(stateDir(env), cwd).catch(() => { });
106
+ await gibson.close();
107
+ },
108
+ };
109
+ registry.batch(() => {
110
+ registry.register(statusTool(() => gibson));
111
+ // A dispatched run is fully decided by its launch: there is nothing to
112
+ // log in to or connect, and no host key to write.
113
+ if (gibson.source !== "dispatched") {
114
+ registry.register(loginTool(surface, env, deps));
115
+ registry.register(connectTool(surface, env, cwd, deps));
116
+ }
117
+ });
118
+ discovery = await registry.batchAsync(() => registerPosture(posture, gibson, postureCtx()));
119
+ return surface;
120
+ }
121
+ /**
122
+ * Which transports this posture holds, for the generated RPC tools.
123
+ *
124
+ * `taskTransport` is the per-turn transport when there is one. The harness
125
+ * object is passed through whole, because the RPC tools also read its
126
+ * `context` to fill `ContextInfo`; only the transport is swapped.
127
+ */
128
+ export function channelsOf(gibson, taskTransport) {
129
+ return {
130
+ ...(gibson.session ? { session: gibson.session.transport } : {}),
131
+ ...(gibson.live ? { task: taskTransport ? { ...gibson.live.harness, transport: taskTransport } : gibson.live.harness } : {}),
132
+ };
133
+ }
134
+ /**
135
+ * The tools a posture carries: one per RPC, one per SDK helper, one per
136
+ * checked-in platform tool.
137
+ *
138
+ * A standalone posture holds no transport, so it carries no RPC tools: a
139
+ * tool with no daemon behind it answers every call with a dial error, which
140
+ * reads to a model like a broken platform rather than an unconnected
141
+ * session. Its helper tools are the ones that need no platform.
142
+ *
143
+ * Returns the discovery poller when this posture has a catalog to poll, so
144
+ * the caller can stop it on upgrade or close.
145
+ */
146
+ export async function registerPosture(group, gibson, ctx) {
147
+ for (const tool of helperToolsFor(gibson, ctx.cwd, ctx.env))
148
+ group.register(tool);
149
+ if (ctx.ask)
150
+ group.register(ctx.ask);
151
+ const channels = channelsOf(gibson, ctx.taskTransport);
152
+ if (channels.session || channels.task) {
153
+ for (const tool of rpcTools({ channels, streamLimit: ctx.streamLimit }))
154
+ group.register(tool);
155
+ }
156
+ await writeHandoff(gibson, ctx);
157
+ // Discovery reads the tenant catalog, which is a ComponentService call, so
158
+ // it needs the component check-in. A dispatched run has none.
159
+ if (!gibson.session)
160
+ return undefined;
161
+ const discovery = startDiscovery({
162
+ group,
163
+ session: gibson.session,
164
+ log,
165
+ ...(ctx.discoveryIntervalMs === undefined ? {} : { intervalMs: ctx.discoveryIntervalMs }),
166
+ });
167
+ try {
168
+ const outcome = await discovery.refresh();
169
+ log(outcome.note ? `${TAG} Gibson tool discovery: ${outcome.note}` : `${TAG} discovery: ${outcome.tools} tool(s), ${outcome.plugins} plugin(s) registered`);
170
+ }
171
+ catch (e) {
172
+ // Discovery is a convenience. gibson_call_tool is registered either way,
173
+ // so a failure here costs the per-tool wrappers only.
174
+ log(`${TAG} Gibson tool discovery failed: ${e.message}`);
175
+ }
176
+ return discovery;
177
+ }
178
+ /**
179
+ * The files a host's hook processes read.
180
+ *
181
+ * A hook runs as its own process and cannot reach this server, so the
182
+ * ambient block and the live-mission coordinates are written beside the host
183
+ * key, one pair per working directory. A dispatched run writes nothing: its
184
+ * launch decided everything, and there is no hook to hand anything to.
185
+ */
186
+ async function writeHandoff(gibson, ctx) {
187
+ if (gibson.source === "dispatched")
188
+ return;
189
+ const dir = stateDir(ctx.env);
190
+ if (gibson.knowledge && ctx.ambient) {
191
+ const block = await ctx.ambient.block().catch(() => "");
192
+ if (block)
193
+ await writeAmbient(dir, ctx.cwd, block).catch((e) => log(`${TAG} ambient handoff failed: ${e.message}`));
194
+ }
195
+ if (gibson.live) {
196
+ await writeLive(dir, ctx.cwd, {
197
+ missionId: gibson.live.missionId,
198
+ workId: gibson.live.workId,
199
+ endpoint: gibson.live.harness.endpoint,
200
+ token: gibson.live.harness.token(),
201
+ insecure: gibson.settings.callbackInsecure,
202
+ writtenAt: Date.now(),
203
+ }).catch((e) => log(`${TAG} live handoff failed: ${e.message}`));
204
+ }
205
+ }
206
+ function instructions() {
207
+ return ("Gibson tools. Call gibson_status to see how this session is connected. " +
208
+ "Without a platform, call gibson_login and then gibson_connect to enroll this host and start a live mission.");
209
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ /**
3
+ * The `gibson` CLI as the session's user. It holds the login session
4
+ * (`gibson login`, a browser device flow), so it is the one process on the
5
+ * host that can mint an enrollment token or create a target. The plugin
6
+ * drives it instead of asking a person to copy tokens between terminals.
7
+ */
8
+ export type Spawn = (cmd: string, args: string[], opts: {
9
+ detached?: boolean;
10
+ env?: NodeJS.ProcessEnv;
11
+ }) => ChildProcess;
12
+ export interface CliOptions {
13
+ bin?: string;
14
+ env?: NodeJS.ProcessEnv;
15
+ spawn?: Spawn;
16
+ timeoutMs?: number;
17
+ }
18
+ export interface CliResult {
19
+ code: number;
20
+ stdout: string;
21
+ stderr: string;
22
+ }
23
+ export declare function runGibson(args: string[], opts?: CliOptions): Promise<CliResult>;
24
+ /** True when the CLI's failure reads as "no login session". */
25
+ export declare function needsLogin(r: CliResult): boolean;
26
+ /**
27
+ * `gibson agent enroll`: mint a one-time bootstrap token for a new machine
28
+ * identity, authenticated as the person who ran `gibson login`.
29
+ */
30
+ /**
31
+ * The session capabilities an interactive coding agent needs in its
32
+ * credential's ceiling (ADR-0045): it starts its own live mission
33
+ * (gibson#1593 decision 9) and may hand sub-tasks to other agents.
34
+ */
35
+ export declare const INTERACTIVE_AGENT_CAPABILITIES: readonly ["mission:originate", "mission:delegate"];
36
+ export declare function enrollIdentity(name: string, opts?: CliOptions & {
37
+ gibsonURL?: string;
38
+ tenant?: string;
39
+ capabilities?: readonly string[];
40
+ }): Promise<string>;
41
+ export interface Target {
42
+ id: string;
43
+ name: string;
44
+ type: string;
45
+ status: string;
46
+ }
47
+ /** `gibson target list`, a tabwriter table: UUID NAME TYPE STATUS. */
48
+ export declare function parseTargetList(stdout: string): Target[];
49
+ export declare function listTargets(opts?: CliOptions & {
50
+ gibsonURL?: string;
51
+ tenant?: string;
52
+ }): Promise<Target[]>;
53
+ /**
54
+ * `gibson target create`: prints the new target id. The daemon refuses a
55
+ * target with neither a URL nor connection parameters, so the URL is
56
+ * required here: for a coding workspace it names the repository.
57
+ */
58
+ export declare function createTarget(name: string, url: string, opts?: CliOptions & {
59
+ gibsonURL?: string;
60
+ tenant?: string;
61
+ type?: string;
62
+ }): Promise<string>;
63
+ export interface LoginPrompt {
64
+ url: string;
65
+ code: string;
66
+ }
67
+ /**
68
+ * `gibson login`: a browser device flow. The CLI prints a URL and a code, then
69
+ * waits for the approval. This starts it detached, returns the URL and code
70
+ * as soon as they appear, and leaves the CLI waiting so the session lands in
71
+ * `~/.gibson/auth/credentials` when the person approves.
72
+ */
73
+ export declare function startLogin(opts?: CliOptions & {
74
+ gibsonURL?: string;
75
+ }): Promise<LoginPrompt>;
76
+ /**
77
+ * The person originates the session mission. A component may originate a
78
+ * mission only from inside one it was dispatched to (gibson ADR-0063), and an
79
+ * interactive session has no parent mission, so the definition goes through
80
+ * the CLI's login session: `gibson mission submit` validates it, registers it
81
+ * and runs it, then prints the mission id. The daemon dispatches the one AGENT
82
+ * node to this component, which must already be checked in.
83
+ */
84
+ export declare function submitMission(definition: unknown, targetId: string, opts?: CliOptions & {
85
+ gibsonURL?: string;
86
+ tenant?: string;
87
+ dir?: string;
88
+ }): Promise<string>;
package/dist/cli.js ADDED
@@ -0,0 +1,186 @@
1
+ import { spawn as nodeSpawn } from "node:child_process";
2
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ export async function runGibson(args, opts = {}) {
6
+ const spawn = opts.spawn ?? nodeSpawn;
7
+ const child = spawn(opts.bin ?? opts.env?.GIBSON_CLI ?? "gibson", args, { env: opts.env ?? process.env });
8
+ return await new Promise((resolve, reject) => {
9
+ const out = [];
10
+ const err = [];
11
+ const timer = setTimeout(() => {
12
+ child.kill();
13
+ reject(new Error(`gibson ${args[0] ?? ""} ${args[1] ?? ""} timed out after ${opts.timeoutMs ?? 60_000}ms`));
14
+ }, opts.timeoutMs ?? 60_000);
15
+ child.stdout?.on("data", (d) => out.push(d.toString()));
16
+ child.stderr?.on("data", (d) => err.push(d.toString()));
17
+ child.on("error", (e) => {
18
+ clearTimeout(timer);
19
+ reject(new Error(`cannot run the gibson CLI (${e.message}). Install it: see https://github.com/zeroroot-ai/adk`));
20
+ });
21
+ child.on("exit", (code) => {
22
+ clearTimeout(timer);
23
+ resolve({ code: code ?? -1, stdout: out.join(""), stderr: err.join("") });
24
+ });
25
+ });
26
+ }
27
+ /** True when the CLI's failure reads as "no login session". */
28
+ export function needsLogin(r) {
29
+ return /login|credentials|unauthenticated|token expired|not signed in/i.test(r.stderr + r.stdout);
30
+ }
31
+ /**
32
+ * `gibson agent enroll`: mint a one-time bootstrap token for a new machine
33
+ * identity, authenticated as the person who ran `gibson login`.
34
+ */
35
+ /**
36
+ * The session capabilities an interactive coding agent needs in its
37
+ * credential's ceiling (ADR-0045): it starts its own live mission
38
+ * (gibson#1593 decision 9) and may hand sub-tasks to other agents.
39
+ */
40
+ export const INTERACTIVE_AGENT_CAPABILITIES = ["mission:originate", "mission:delegate"];
41
+ export async function enrollIdentity(name, opts = {}) {
42
+ const args = ["agent", "enroll", "--name", name, "--kind", "agent"];
43
+ for (const c of opts.capabilities ?? INTERACTIVE_AGENT_CAPABILITIES)
44
+ args.push("--capability", c);
45
+ if (opts.gibsonURL)
46
+ args.push("--gibson-url", opts.gibsonURL);
47
+ if (opts.tenant)
48
+ args.push("--tenant", opts.tenant);
49
+ const r = await runGibson(args, opts);
50
+ const m = /^bootstrap_token:\s*(\S+)/m.exec(r.stdout);
51
+ if (r.code !== 0 || !m) {
52
+ const why = (r.stderr || r.stdout).trim();
53
+ throw new Error(needsLogin(r)
54
+ ? `the gibson CLI has no login session (${why}). Call gibson_login first.`
55
+ : `gibson agent enroll failed (exit ${r.code}): ${why || "no bootstrap_token in the output"}`);
56
+ }
57
+ return m[1];
58
+ }
59
+ /** `gibson target list`, a tabwriter table: UUID NAME TYPE STATUS. */
60
+ export function parseTargetList(stdout) {
61
+ return stdout
62
+ .split("\n")
63
+ .map((l) => l.trim())
64
+ .filter((l) => l && !/^UUID\s+NAME/.test(l))
65
+ .map((l) => l.split(/\s{2,}|\t/))
66
+ .filter((cols) => cols.length >= 2)
67
+ .map(([id, name, type, status]) => ({ id: id, name: name ?? "", type: type ?? "", status: status ?? "" }));
68
+ }
69
+ export async function listTargets(opts = {}) {
70
+ const args = ["target", "list"];
71
+ if (opts.gibsonURL)
72
+ args.push("--gibson-url", opts.gibsonURL);
73
+ if (opts.tenant)
74
+ args.push("--tenant", opts.tenant);
75
+ const r = await runGibson(args, opts);
76
+ if (r.code !== 0) {
77
+ const why = (r.stderr || r.stdout).trim();
78
+ throw new Error(needsLogin(r) ? `the gibson CLI has no login session (${why}). Call gibson_login first.` : `gibson target list failed: ${why}`);
79
+ }
80
+ return parseTargetList(r.stdout);
81
+ }
82
+ /**
83
+ * `gibson target create`: prints the new target id. The daemon refuses a
84
+ * target with neither a URL nor connection parameters, so the URL is
85
+ * required here: for a coding workspace it names the repository.
86
+ */
87
+ export async function createTarget(name, url, opts = {}) {
88
+ const args = ["target", "create", "--name", name, "--url", url, "--type", opts.type ?? "custom"];
89
+ if (opts.gibsonURL)
90
+ args.push("--gibson-url", opts.gibsonURL);
91
+ if (opts.tenant)
92
+ args.push("--tenant", opts.tenant);
93
+ const r = await runGibson(args, opts);
94
+ const id = r.stdout.trim().split("\n").pop()?.trim() ?? "";
95
+ if (r.code !== 0 || !id) {
96
+ const why = (r.stderr || r.stdout).trim();
97
+ throw new Error(needsLogin(r) ? `the gibson CLI has no login session (${why}). Call gibson_login first.` : `gibson target create failed: ${why}`);
98
+ }
99
+ return id;
100
+ }
101
+ /**
102
+ * `gibson login`: a browser device flow. The CLI prints a URL and a code, then
103
+ * waits for the approval. This starts it detached, returns the URL and code
104
+ * as soon as they appear, and leaves the CLI waiting so the session lands in
105
+ * `~/.gibson/auth/credentials` when the person approves.
106
+ */
107
+ export async function startLogin(opts = {}) {
108
+ const spawn = opts.spawn ?? nodeSpawn;
109
+ const args = ["login"];
110
+ if (opts.gibsonURL)
111
+ args.push("--gibson-url", opts.gibsonURL);
112
+ const child = spawn(opts.bin ?? opts.env?.GIBSON_CLI ?? "gibson", args, { detached: true, env: opts.env ?? process.env });
113
+ return await new Promise((resolve, reject) => {
114
+ let buf = "";
115
+ const timer = setTimeout(() => {
116
+ child.kill();
117
+ reject(new Error(`gibson login printed no device code within ${opts.timeoutMs ?? 30_000}ms: ${buf.trim()}`));
118
+ }, opts.timeoutMs ?? 30_000);
119
+ const check = () => {
120
+ const url = /open:\s*\n?\s*(https?:\/\/\S+)/.exec(buf);
121
+ const code = /confirm this code:\s*(\S+)/.exec(buf);
122
+ if (url && code) {
123
+ clearTimeout(timer);
124
+ child.unref();
125
+ resolve({ url: url[1], code: code[1] });
126
+ }
127
+ };
128
+ child.stdout?.on("data", (d) => {
129
+ buf += d.toString();
130
+ check();
131
+ });
132
+ child.stderr?.on("data", (d) => {
133
+ buf += d.toString();
134
+ check();
135
+ });
136
+ child.on("error", (e) => {
137
+ clearTimeout(timer);
138
+ reject(new Error(`cannot run the gibson CLI (${e.message}). Install it: see https://github.com/zeroroot-ai/adk`));
139
+ });
140
+ child.on("exit", (code) => {
141
+ clearTimeout(timer);
142
+ reject(new Error(`gibson login exited with ${code} before printing a device code: ${buf.trim()}`));
143
+ });
144
+ });
145
+ }
146
+ const UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/g;
147
+ /**
148
+ * The person originates the session mission. A component may originate a
149
+ * mission only from inside one it was dispatched to (gibson ADR-0063), and an
150
+ * interactive session has no parent mission, so the definition goes through
151
+ * the CLI's login session: `gibson mission submit` validates it, registers it
152
+ * and runs it, then prints the mission id. The daemon dispatches the one AGENT
153
+ * node to this component, which must already be checked in.
154
+ */
155
+ export async function submitMission(definition, targetId, opts = {}) {
156
+ const dir = await mkdtemp(join(opts.dir ?? tmpdir(), "gibson-mcp-mission-"));
157
+ const file = join(dir, "session.json");
158
+ try {
159
+ await writeFile(file, JSON.stringify(definition), { mode: 0o600 });
160
+ // --detach: the CLI returns once the daemon started the run and prints the
161
+ // mission id. Without it the CLI holds the RunMission stream until the
162
+ // mission ends, which never happens: this process must go on to claim the
163
+ // dispatch, and the CLI's own deadline fires first.
164
+ const args = ["mission", "submit", file, "--format", "json", "--target", targetId, "--detach"];
165
+ if (opts.gibsonURL)
166
+ args.push("--gibson-url", opts.gibsonURL);
167
+ if (opts.tenant)
168
+ args.push("--tenant", opts.tenant);
169
+ const r = await runGibson(args, { ...opts, timeoutMs: opts.timeoutMs ?? 120_000 });
170
+ if (r.code !== 0) {
171
+ if (needsLogin(r))
172
+ throw new Error("the gibson CLI has no session; run gibson_login first");
173
+ if (/unknown flag: --detach/.test(r.stderr))
174
+ throw new Error("the gibson CLI is too old for mission submit --detach; update the gibson CLI (adk >= 0.109)");
175
+ throw new Error(`gibson mission submit failed (exit ${r.code}): ${(r.stderr || r.stdout).trim().slice(0, 400)}`);
176
+ }
177
+ const ids = r.stdout.match(UUID) ?? [];
178
+ const missionId = ids[ids.length - 1];
179
+ if (!missionId)
180
+ throw new Error(`gibson mission submit printed no mission id: ${r.stdout.trim().slice(0, 400)}`);
181
+ return missionId;
182
+ }
183
+ finally {
184
+ await rm(dir, { recursive: true, force: true });
185
+ }
186
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Where a session learns how to reach Gibson, in precedence order:
3
+ *
4
+ * 1. the environment (`GIBSON_*`), for people who script it,
5
+ * 2. the config this server wrote after a `gibson_connect`,
6
+ * 3. the `gibson` CLI login session (`~/.gibson/auth/credentials`), which
7
+ * already knows the platform URL, the active tenant and the private CA.
8
+ *
9
+ * So a person who ran `gibson login` once needs nothing else: the server
10
+ * finds the platform through the same session the CLI uses.
11
+ */
12
+ export interface ServerConfig {
13
+ platformURL?: string;
14
+ daemonURL?: string;
15
+ targetId?: string;
16
+ caCertPath?: string;
17
+ callbackInsecure?: boolean;
18
+ }
19
+ export interface CliCredentials {
20
+ gibsonURL?: string;
21
+ tenant?: string;
22
+ caCertPath?: string;
23
+ }
24
+ export interface Settings {
25
+ platformURL?: string;
26
+ daemonURL?: string;
27
+ targetId?: string;
28
+ caCertPath?: string;
29
+ callbackInsecure: boolean;
30
+ bootstrapToken?: string;
31
+ hostKeyPath: string;
32
+ agentName: string;
33
+ tenant?: string;
34
+ }
35
+ /** The registered component name, and the `sub` of every grant minted for it. */
36
+ export declare const DEFAULT_AGENT_NAME = "gibson-mcp";
37
+ export declare function configPath(env: NodeJS.ProcessEnv): string;
38
+ export declare function readConfig(env: NodeJS.ProcessEnv): Promise<ServerConfig>;
39
+ /** Merge and persist. Only the keys given change. */
40
+ export declare function writeConfig(env: NodeJS.ProcessEnv, patch: ServerConfig): Promise<ServerConfig>;
41
+ export declare function cliCredentialsPath(env: NodeJS.ProcessEnv): string;
42
+ /** The `gibson` CLI login session, addressing fields only. Tokens are never read. */
43
+ export declare function cliCredentials(env: NodeJS.ProcessEnv): Promise<CliCredentials | undefined>;
44
+ export declare function resolveSettings(env: NodeJS.ProcessEnv, cfg: ServerConfig, creds: CliCredentials | undefined): Settings;
45
+ export declare function loadSettings(env: NodeJS.ProcessEnv): Promise<Settings>;
package/dist/config.js ADDED
@@ -0,0 +1,54 @@
1
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { stateDir } from "./state.js";
5
+ /** The registered component name, and the `sub` of every grant minted for it. */
6
+ export const DEFAULT_AGENT_NAME = "gibson-mcp";
7
+ export function configPath(env) {
8
+ return join(stateDir(env), "config.json");
9
+ }
10
+ export async function readConfig(env) {
11
+ try {
12
+ return JSON.parse(await readFile(configPath(env), "utf8"));
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ /** Merge and persist. Only the keys given change. */
19
+ export async function writeConfig(env, patch) {
20
+ const merged = { ...(await readConfig(env)), ...patch };
21
+ await mkdir(stateDir(env), { recursive: true, mode: 0o700 });
22
+ await writeFile(configPath(env), `${JSON.stringify(merged, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
23
+ return merged;
24
+ }
25
+ export function cliCredentialsPath(env) {
26
+ return env.GIBSON_CLI_CREDENTIALS ?? join(homedir(), ".gibson", "auth", "credentials");
27
+ }
28
+ /** The `gibson` CLI login session, addressing fields only. Tokens are never read. */
29
+ export async function cliCredentials(env) {
30
+ try {
31
+ const raw = JSON.parse(await readFile(cliCredentialsPath(env), "utf8"));
32
+ const str = (k) => (typeof raw[k] === "string" && raw[k] ? raw[k] : undefined);
33
+ return { gibsonURL: str("gibson_url"), tenant: str("active_tenant"), caCertPath: str("ca_cert_path") };
34
+ }
35
+ catch {
36
+ return undefined;
37
+ }
38
+ }
39
+ export function resolveSettings(env, cfg, creds) {
40
+ return {
41
+ platformURL: env.GIBSON_PLATFORM_URL ?? cfg.platformURL ?? creds?.gibsonURL,
42
+ daemonURL: env.GIBSON_DAEMON_URL ?? cfg.daemonURL,
43
+ targetId: env.GIBSON_TARGET_ID ?? cfg.targetId,
44
+ caCertPath: env.GIBSON_CA_CERT ?? cfg.caCertPath ?? creds?.caCertPath,
45
+ callbackInsecure: env.GIBSON_CALLBACK_INSECURE === "1" || cfg.callbackInsecure === true,
46
+ bootstrapToken: env.GIBSON_BOOTSTRAP_TOKEN,
47
+ hostKeyPath: env.GIBSON_HOST_KEY_PATH ?? join(homedir(), ".zerocool", "host.key"),
48
+ agentName: env.ZEROCOOL_AGENT_NAME ?? DEFAULT_AGENT_NAME,
49
+ tenant: creds?.tenant,
50
+ };
51
+ }
52
+ export async function loadSettings(env) {
53
+ return resolveSettings(env, await readConfig(env), await cliCredentials(env));
54
+ }
@@ -0,0 +1,57 @@
1
+ import { type GibsonPlugin, type GibsonSession, type GibsonTool } from "@zeroroot-ai/sdk";
2
+ import { type Log } from "./log.js";
3
+ import type { ToolDefinition, ToolGroup } from "./registry.js";
4
+ /**
5
+ * Checked-in platform tools and plugins, discovered at runtime.
6
+ *
7
+ * The fleet changes while a session runs: a tool a person enrols now should
8
+ * be callable in the same session. So discovery repeats, and the registry's
9
+ * `tools/list_changed` notification is what tells every attached host to
10
+ * ask again.
11
+ *
12
+ * Each discovered tool registers on its own, named `gibson_<tool>`, so the
13
+ * model sees its description rather than one opaque dispatcher. The catalog
14
+ * carries no JSON Schema, only a proto message type name, so the wrapper
15
+ * takes one free-form `input` object and names the type it must match.
16
+ * `gibson_call_tool` stays registered either way: discovery answers
17
+ * `Unimplemented` on some daemons (gibson#1186).
18
+ */
19
+ export declare const DISCOVERY_INTERVAL_MS = 60000;
20
+ /** `gibson_<tool>`, with anything not legal in an MCP tool name replaced. */
21
+ export declare function toolKey(name: string): string;
22
+ export declare function pluginKey(name: string): string;
23
+ export declare function discoveredTool(session: GibsonSession, t: GibsonTool): ToolDefinition;
24
+ export declare function discoveredPlugin(session: GibsonSession, p: GibsonPlugin): ToolDefinition;
25
+ export interface DiscoveryOutcome {
26
+ tools: number;
27
+ plugins: number;
28
+ /** Set when discovery could not run. The set is unchanged, and nothing failed. */
29
+ note?: string;
30
+ /** True when this pass added or removed something. */
31
+ changed: boolean;
32
+ }
33
+ export interface DiscoveryOptions {
34
+ /**
35
+ * Where discovered tools are registered. A group, not the registry, so
36
+ * they leave with the posture that discovered them.
37
+ */
38
+ group: ToolGroup;
39
+ session: GibsonSession;
40
+ log: Log;
41
+ /** How often to look again. `0` runs one pass and stops. */
42
+ intervalMs?: number;
43
+ }
44
+ export interface Discovery {
45
+ /** Run one pass now. */
46
+ refresh(): Promise<DiscoveryOutcome>;
47
+ /** Stop polling. Registered tools stay. */
48
+ stop(): void;
49
+ }
50
+ /**
51
+ * Poll the catalog and keep the registered set equal to it.
52
+ *
53
+ * A pass that cannot reach the catalog leaves the set alone. Dropping every
54
+ * discovered tool because one poll failed would take working tools away from
55
+ * a session mid-task over a transient fault.
56
+ */
57
+ export declare function startDiscovery(opts: DiscoveryOptions): Discovery;