@ravefox/space 0.0.0-stage → 0.2.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 +203 -0
- package/NOTICE +44 -0
- package/README.md +49 -2
- package/assets/webhook-sdk/python/bot.py +63 -0
- package/assets/webhook-sdk/python/commonly.py +232 -0
- package/node_modules/@commonlyai/seats/README.md +25 -0
- package/node_modules/@commonlyai/seats/package.json +22 -0
- package/node_modules/@commonlyai/seats/src/adapters/hermes.js +264 -0
- package/node_modules/@commonlyai/seats/src/adapters/stub.js +24 -0
- package/node_modules/@commonlyai/seats/src/contract.js +48 -0
- package/node_modules/@commonlyai/seats/src/failures.js +44 -0
- package/node_modules/@commonlyai/seats/src/harness-plan.js +329 -0
- package/node_modules/@commonlyai/seats/src/index.js +22 -0
- package/node_modules/@commonlyai/seats/src/process-group.js +269 -0
- package/node_modules/@commonlyai/seats/src/record/format.js +737 -0
- package/node_modules/@commonlyai/seats/src/record/otel.js +238 -0
- package/node_modules/@commonlyai/seats/src/runtime-observation.js +29 -0
- package/node_modules/@commonlyai/seats/src/seat-model.js +17 -0
- package/node_modules/@commonlyai/seats/src/supervisor.js +308 -0
- package/node_modules/@commonlyai/seats/src/trace/events.js +46 -0
- package/node_modules/@commonlyai/seats/src/trace/store.js +276 -0
- package/node_modules/@commonlyai/seats/src/turn-processes.js +226 -0
- package/node_modules/@commonlyai/seats/src/upstream-refusal.js +418 -0
- package/node_modules/@commonlyai/seats/src/valet/env.js +201 -0
- package/node_modules/@commonlyai/seats/src/valet/grant.js +123 -0
- package/node_modules/@commonlyai/seats/src/valet/permissions.js +213 -0
- package/node_modules/@commonlyai/seats/src/wire/provider-proxy.js +529 -0
- package/package.json +56 -4
- package/skills/commonly/SKILL.md +233 -0
- package/src/commands/agent.js +6334 -0
- package/src/commands/daemon.js +933 -0
- package/src/commands/dev.js +153 -0
- package/src/commands/doctor.js +141 -0
- package/src/commands/inbox.js +330 -0
- package/src/commands/login.js +194 -0
- package/src/commands/mcp.js +56 -0
- package/src/commands/pod.js +120 -0
- package/src/commands/task.js +279 -0
- package/src/commands/verify.js +48 -0
- package/src/index.js +99 -0
- package/src/lib/adapter-login.js +76 -0
- package/src/lib/adapters/broker-bridge-entry.js +76 -0
- package/src/lib/adapters/claude.js +2207 -0
- package/src/lib/adapters/codex-broker-bridge.mjs +298 -0
- package/src/lib/adapters/codex.js +1898 -0
- package/src/lib/adapters/dsh.js +513 -0
- package/src/lib/adapters/goose.js +510 -0
- package/src/lib/adapters/hermes.js +18 -0
- package/src/lib/adapters/index.js +68 -0
- package/src/lib/adapters/opencode.js +1681 -0
- package/src/lib/adapters/pi-commonly-mcp.mjs +71 -0
- package/src/lib/adapters/pi-mcp-client.mjs +617 -0
- package/src/lib/adapters/pi-shell-env.mjs +41 -0
- package/src/lib/adapters/pi.js +1025 -0
- package/src/lib/adapters/qwen.js +523 -0
- package/src/lib/adapters/seat-model.js +8 -0
- package/src/lib/adapters/stub.js +1 -0
- package/src/lib/adapters/tested-version.js +106 -0
- package/src/lib/api.js +205 -0
- package/src/lib/claim-outcome.js +128 -0
- package/src/lib/claimed-work-gate.js +73 -0
- package/src/lib/cli-hint.js +33 -0
- package/src/lib/commit-attribution.js +391 -0
- package/src/lib/config.js +155 -0
- package/src/lib/content-refusal.js +94 -0
- package/src/lib/credential-file.js +208 -0
- package/src/lib/daemon-agent-states.js +95 -0
- package/src/lib/daemon-hint.js +18 -0
- package/src/lib/daemon-logs.js +43 -0
- package/src/lib/daemon-service-windows.js +263 -0
- package/src/lib/daemon-service.js +486 -0
- package/src/lib/daemon-state.js +84 -0
- package/src/lib/daemon-store.js +119 -0
- package/src/lib/daemon-supervisor.js +629 -0
- package/src/lib/daemon-watch.js +115 -0
- package/src/lib/declared-mcp-guard.js +284 -0
- package/src/lib/default-environment.js +207 -0
- package/src/lib/device-login.js +126 -0
- package/src/lib/digest-frame.js +151 -0
- package/src/lib/doctor.js +291 -0
- package/src/lib/egress-proxy.js +160 -0
- package/src/lib/enforcement.js +946 -0
- package/src/lib/environment.js +681 -0
- package/src/lib/first-output-watchdog.js +191 -0
- package/src/lib/git-readonly-server.js +138 -0
- package/src/lib/git-readonly.js +463 -0
- package/src/lib/git-remote-commonly.mjs +100 -0
- package/src/lib/grant-broker-guard.js +345 -0
- package/src/lib/hooks-config.js +238 -0
- package/src/lib/host-gh-broker.js +152 -0
- package/src/lib/host-gh-shim.mjs +98 -0
- package/src/lib/inbox.js +483 -0
- package/src/lib/instance-proxy.js +76 -0
- package/src/lib/machine-host.js +29 -0
- package/src/lib/mcp-credential-delivery.js +121 -0
- package/src/lib/mcp-first-run.js +50 -0
- package/src/lib/mcp-home.js +644 -0
- package/src/lib/mcp-ready.js +68 -0
- package/src/lib/mcp-server-version.js +123 -0
- package/src/lib/mcp-token.js +36 -0
- package/src/lib/mcp-warm-child.mjs +412 -0
- package/src/lib/memory-bridge.js +191 -0
- package/src/lib/memory-import.js +156 -0
- package/src/lib/missing-harness.js +31 -0
- package/src/lib/model-unavailable.js +154 -0
- package/src/lib/native-runtime-inventory.js +40 -0
- package/src/lib/outsider-grant-mcp.js +82 -0
- package/src/lib/own-key-launch.js +42 -0
- package/src/lib/pod-catch-up.js +55 -0
- package/src/lib/pod-focus.js +224 -0
- package/src/lib/pod-tail.js +68 -0
- package/src/lib/poll-liveness.js +103 -0
- package/src/lib/poll-retry.js +81 -0
- package/src/lib/poller.js +200 -0
- package/src/lib/provider-proxy.js +42 -0
- package/src/lib/public-seat-mcp-flag.js +36 -0
- package/src/lib/public-tool-error.js +60 -0
- package/src/lib/public-withheld-tools.js +48 -0
- package/src/lib/reply-receipt.js +55 -0
- package/src/lib/resolve-on-path.js +22 -0
- package/src/lib/runtime-inventory.js +56 -0
- package/src/lib/runtime-observation.js +37 -0
- package/src/lib/sandbox/bwrap.js +275 -0
- package/src/lib/sandbox/codex-perimeter.js +16 -0
- package/src/lib/sandbox/host-check.js +268 -0
- package/src/lib/sandbox/mode.js +49 -0
- package/src/lib/sandbox/opencode-loopback.mjs +68 -0
- package/src/lib/sandbox/seatbelt.js +684 -0
- package/src/lib/sandbox/turn-dir.js +56 -0
- package/src/lib/sandbox/windows-unsandboxed.js +183 -0
- package/src/lib/sandbox/workspace-git-config.js +243 -0
- package/src/lib/sandbox/workspace-pin.js +158 -0
- package/src/lib/seat-runtime-controls.js +61 -0
- package/src/lib/seat-stop.js +97 -0
- package/src/lib/server-skills.js +32 -0
- package/src/lib/session-recycle.js +219 -0
- package/src/lib/session-store.js +215 -0
- package/src/lib/skill-prompt.js +74 -0
- package/src/lib/skills-import.js +113 -0
- package/src/lib/spawn-credential.js +402 -0
- package/src/lib/spawn-retry.js +311 -0
- package/src/lib/stable-install.js +176 -0
- package/src/lib/supervised-turn.js +352 -0
- package/src/lib/task.js +235 -0
- package/src/lib/turn-failure-notice.js +177 -0
- package/src/lib/unconfined-seat-guard.js +94 -0
- package/src/lib/update-check.js +192 -0
- package/src/lib/upstream-refusal.js +1 -0
- package/src/lib/webhook-server.js +109 -0
- package/src/lib/windows-command.js +393 -0
- package/src/lib/workspace-git.js +87 -0
- package/src/lib/workspace-trust.js +162 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Repository verification entry point; scenario implementations live in backend. */
|
|
2
|
+
import { existsSync } from 'fs';
|
|
3
|
+
import { resolve, join } from 'path';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
|
|
6
|
+
export const SCENARIOS = {
|
|
7
|
+
wave1: '__tests__/service/verify.wave1.test.js',
|
|
8
|
+
'connector-grant': '__tests__/service/verify.connectorGrant.test.js',
|
|
9
|
+
};
|
|
10
|
+
const testDatabase = (name) => /^(test|verify)([-_0-9]|$)|[-_]test$/.test(name || '');
|
|
11
|
+
|
|
12
|
+
export const verificationPlan = (scenario, repo, env = process.env) => {
|
|
13
|
+
const suite = SCENARIOS[scenario];
|
|
14
|
+
if (!suite) throw new Error(`Unknown scenario: ${scenario}. Choose ${Object.keys(SCENARIOS).join(', ')}.`);
|
|
15
|
+
let mongoDatabase = '';
|
|
16
|
+
// MongoDB authorities can contain comma-separated seed hosts, unlike WHATWG URLs.
|
|
17
|
+
const mongoPath = /^mongodb(?:\+srv)?:\/\/[^/?#]+\/([^?#]*)/.exec(env.MONGO_URI || '');
|
|
18
|
+
try { mongoDatabase = decodeURIComponent(mongoPath?.[1] || ''); } catch { /* refused below */ }
|
|
19
|
+
if (!env.PG_HOST || !testDatabase(env.PG_DATABASE) || !testDatabase(mongoDatabase)) {
|
|
20
|
+
throw new Error('Set MONGO_URI, PG_HOST and PG_DATABASE to dedicated test/verify databases. Fixture setup clears their data.');
|
|
21
|
+
}
|
|
22
|
+
const cwd = resolve(repo, 'backend');
|
|
23
|
+
const runner = join(cwd, 'node_modules/jest/bin/jest.js');
|
|
24
|
+
if (!existsSync(join(cwd, suite)) || !existsSync(runner)) {
|
|
25
|
+
throw new Error('Use --repo with a Space checkout containing the scenario and installed backend dependencies.');
|
|
26
|
+
}
|
|
27
|
+
return { command: process.execPath, args: [runner, '--runInBand', '--forceExit', '--runTestsByPath', suite,
|
|
28
|
+
...(env.COMMONLY_VERIFY_CACHE_DIR ? ['--cacheDirectory', env.COMMONLY_VERIFY_CACHE_DIR] : [])],
|
|
29
|
+
options: { cwd, stdio: 'inherit', env: { ...env, INTEGRATION_TEST: 'true' } } };
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const registerVerify = (program, run = spawnSync) => {
|
|
33
|
+
program.command('verify <scenario>')
|
|
34
|
+
.description(`Run shared real-HTTP scenarios against isolated test databases (${Object.keys(SCENARIOS).join(', ')})`)
|
|
35
|
+
.option('--repo <path>', 'Space checkout with backend test dependencies', process.cwd())
|
|
36
|
+
.addHelpText('after', '\nUse Node 22 and explicit MONGO_URI, PG_HOST and PG_DATABASE. Fixture databases are cleared.\nRun inside the bounded test container when available; this command does not build images or contact a live instance.\n')
|
|
37
|
+
.action((scenario, options) => {
|
|
38
|
+
try {
|
|
39
|
+
const plan = verificationPlan(scenario, options.repo);
|
|
40
|
+
const result = run(plan.command, plan.args, plan.options);
|
|
41
|
+
if (result.error) throw result.error;
|
|
42
|
+
process.exitCode = result.status === null ? 1 : result.status;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error(error.message);
|
|
45
|
+
process.exitCode = 1;
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @commonlyai/cli — the developer interface to CAP.
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* commonly login
|
|
7
|
+
* commonly agent connect --name my-agent --port 3001
|
|
8
|
+
* commonly pod send <podId> "hello"
|
|
9
|
+
* commonly dev up
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Command } from 'commander';
|
|
13
|
+
import { readFileSync } from 'fs';
|
|
14
|
+
import { join, dirname } from 'path';
|
|
15
|
+
import { fileURLToPath } from 'url';
|
|
16
|
+
|
|
17
|
+
import { registerLogin, registerWhoami } from './commands/login.js';
|
|
18
|
+
import { registerInbox } from './commands/inbox.js';
|
|
19
|
+
import { registerAgent } from './commands/agent.js';
|
|
20
|
+
import { registerDaemon } from './commands/daemon.js';
|
|
21
|
+
import { registerPod } from './commands/pod.js';
|
|
22
|
+
import { registerDev } from './commands/dev.js';
|
|
23
|
+
import { registerVerify } from './commands/verify.js';
|
|
24
|
+
import { registerDoctor } from './commands/doctor.js';
|
|
25
|
+
import { registerMcp } from './commands/mcp.js';
|
|
26
|
+
import { registerTask } from './commands/task.js';
|
|
27
|
+
import { CLI } from './lib/cli-hint.js';
|
|
28
|
+
|
|
29
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
30
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
31
|
+
|
|
32
|
+
const program = new Command();
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.name('space')
|
|
36
|
+
.description('The Space CLI — connect agents, manage pods, iterate fast')
|
|
37
|
+
.version(pkg.version)
|
|
38
|
+
.showHelpAfterError('(run `space --help` for usage)');
|
|
39
|
+
|
|
40
|
+
// Auth
|
|
41
|
+
registerLogin(program);
|
|
42
|
+
registerWhoami(program);
|
|
43
|
+
|
|
44
|
+
// An operator account's own attention queue (explicit token file, never the
|
|
45
|
+
// saved login — see cli/src/lib/inbox.js)
|
|
46
|
+
registerInbox(program);
|
|
47
|
+
|
|
48
|
+
// Agent management
|
|
49
|
+
registerAgent(program);
|
|
50
|
+
|
|
51
|
+
// Local daemon lifecycle
|
|
52
|
+
registerDaemon(program);
|
|
53
|
+
|
|
54
|
+
// Pod management
|
|
55
|
+
registerPod(program);
|
|
56
|
+
|
|
57
|
+
// Local dev environment
|
|
58
|
+
registerDev(program);
|
|
59
|
+
registerVerify(program);
|
|
60
|
+
|
|
61
|
+
// Install health: one line per check (TASK-660)
|
|
62
|
+
registerDoctor(program);
|
|
63
|
+
registerMcp(program);
|
|
64
|
+
|
|
65
|
+
// The task board as an agent: list, create, claim, update, close (TASK-703)
|
|
66
|
+
registerTask(program);
|
|
67
|
+
|
|
68
|
+
// Each subcommand owns its own `--instance <url>` flag; a program-level
|
|
69
|
+
// duplicate shadowed the subcommand value on commander v12, causing
|
|
70
|
+
// `commonly login --instance …` to silently fall back to the default URL.
|
|
71
|
+
|
|
72
|
+
program.addHelpText('after', `
|
|
73
|
+
Quick start:
|
|
74
|
+
$ ${CLI} login --instance https://api.commonly.me --key dev
|
|
75
|
+
$ ${CLI} agent attach claude --pod <podId> --name my-claude
|
|
76
|
+
$ ${CLI} agent run my-claude # Ctrl+C to stop
|
|
77
|
+
$ ${CLI} agent detach my-claude # clean uninstall
|
|
78
|
+
$ ${CLI} daemon register --name "My MacBook"
|
|
79
|
+
$ ${CLI} daemon status
|
|
80
|
+
$ ${CLI} doctor # check Node, install, login, daemon, adapters
|
|
81
|
+
$ ${CLI} inbox list --token-file ~/.commonly/bin/ops-token # an operator's own queue
|
|
82
|
+
|
|
83
|
+
Custom Python agent:
|
|
84
|
+
$ ${CLI} agent init --language python --name research-bot --pod <podId>
|
|
85
|
+
$ python3 research-bot.py
|
|
86
|
+
|
|
87
|
+
Subcommand help:
|
|
88
|
+
$ ${CLI} login --help
|
|
89
|
+
$ ${CLI} agent attach --help
|
|
90
|
+
$ ${CLI} agent run --help
|
|
91
|
+
$ ${CLI} pod tail --help
|
|
92
|
+
|
|
93
|
+
Note: --instance is a subcommand option, not a program option. It accepts
|
|
94
|
+
either a saved key name ("dev", "local", "default") or a full URL.
|
|
95
|
+
|
|
96
|
+
Docs: https://github.com/grootbro/space/blob/space/bootstrap/docs/cli/README.md
|
|
97
|
+
`);
|
|
98
|
+
|
|
99
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
|
|
3
|
+
// Whether each installed CLI adapter is signed in, by the exit code of its own
|
|
4
|
+
// status command. A stranger on a fresh computer otherwise learns it from a
|
|
5
|
+
// failed @mention ("claude exited 1: Not logged in"), after setup said all was
|
|
6
|
+
// well (Growth's F370 hand walk). Only the exit code is read: the commands'
|
|
7
|
+
// output, which names the account, is never captured or printed.
|
|
8
|
+
//
|
|
9
|
+
// Both commands exit 1 when signed out (checked 2026-11-10 under an empty HOME:
|
|
10
|
+
// `claude auth status --json`, `codex login status`). opencode is left out: its
|
|
11
|
+
// `auth list` was not checked, and a guessed probe would cry wolf.
|
|
12
|
+
export const LOGIN_STATUS_ARGS = {
|
|
13
|
+
claude: ['auth', 'status', '--json'],
|
|
14
|
+
codex: ['login', 'status'],
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Credentials a seat runs on without a CLI login, so the probe is skipped.
|
|
18
|
+
// codex's are the ones codexAuthKind and the proxy route accept (adapters/
|
|
19
|
+
// codex.js), even though `codex login status` exits 1 with only a key set.
|
|
20
|
+
const KEY_ENV = {
|
|
21
|
+
claude: ['ANTHROPIC_API_KEY'],
|
|
22
|
+
codex: ['OPENAI_API_KEY', 'CODEX_API_KEY', 'CODEX_ACCESS_TOKEN'],
|
|
23
|
+
};
|
|
24
|
+
const envSet = (env, name) => typeof env[name] === 'string' && env[name].trim() !== '';
|
|
25
|
+
|
|
26
|
+
// Both CLIs exit exactly 1 when signed out. Any other non-zero code (2 for an
|
|
27
|
+
// unknown subcommand on an old CLI) is no verdict, not "signed out".
|
|
28
|
+
const SIGNED_OUT_EXIT = 1;
|
|
29
|
+
|
|
30
|
+
export const signedOutHint = (name) => `${name} is installed but not signed in; run \`${name}\` once to log in before adding an agent`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The installed adapters that report signed out. `detect(name)` resolves truthy
|
|
34
|
+
* when the CLI is installed; `runStatus(name, args)` resolves its exit code, or
|
|
35
|
+
* null when it could not be run (timeout, spawn error), which is not a verdict.
|
|
36
|
+
*/
|
|
37
|
+
export const signedOutAdapters = async ({ detect, runStatus, env = process.env, names = Object.keys(LOGIN_STATUS_ARGS) }) => {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const name of names) {
|
|
40
|
+
if (!LOGIN_STATUS_ARGS[name] || (KEY_ENV[name] || []).some((key) => envSet(env, key))) continue;
|
|
41
|
+
let installed = false;
|
|
42
|
+
try { installed = Boolean(await detect(name)); } catch { /* not installed */ }
|
|
43
|
+
if (!installed) continue;
|
|
44
|
+
const code = await runStatus(name, LOGIN_STATUS_ARGS[name]).catch(() => null);
|
|
45
|
+
if (code === SIGNED_OUT_EXIT) out.push(name);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One harness's login, for the daemon's inventory (F458): true signed in (exit
|
|
52
|
+
* 0), false signed out (exit 1), null when there is no verdict: no checked
|
|
53
|
+
* probe for it, a key in the environment it runs on instead, or a probe that
|
|
54
|
+
* couldn't run. Only the exit code is read, as above.
|
|
55
|
+
*/
|
|
56
|
+
export const loginState = async (name, { runStatus = runLoginStatus, env = process.env } = {}) => {
|
|
57
|
+
if (!LOGIN_STATUS_ARGS[name] || (KEY_ENV[name] || []).some((key) => envSet(env, key))) return null;
|
|
58
|
+
const code = await runStatus(name, LOGIN_STATUS_ARGS[name]).catch(() => null);
|
|
59
|
+
if (code === 0) return true;
|
|
60
|
+
return code === SIGNED_OUT_EXIT ? false : null;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// The exit code of `<name> <args>`, with every stream discarded. null when it
|
|
64
|
+
// cannot run or takes longer than `timeoutMs` (a CLI that opens a prompt).
|
|
65
|
+
export const runLoginStatus = (name, args, { timeoutMs = 10_000, spawnFn = spawn } = {}) => new Promise((done) => {
|
|
66
|
+
let child;
|
|
67
|
+
try {
|
|
68
|
+
child = spawnFn(name, args, { stdio: 'ignore', windowsHide: true, shell: process.platform === 'win32' });
|
|
69
|
+
} catch {
|
|
70
|
+
done(null);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const timer = setTimeout(() => { child.kill(); done(null); }, timeoutMs);
|
|
74
|
+
child.on('error', () => { clearTimeout(timer); done(null); });
|
|
75
|
+
child.on('exit', (code) => { clearTimeout(timer); done(typeof code === 'number' ? code : null); });
|
|
76
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stdio server a harness runs to reach the grant broker (F175b, F175d).
|
|
3
|
+
*
|
|
4
|
+
* Codex, Qwen and Goose can't hold the broker's bearer token in an HTTP entry
|
|
5
|
+
* without putting it somewhere the rule forbids: Codex reads it from an env
|
|
6
|
+
* var, Qwen sends `--mcp-config` headers literally, and Goose fills a header
|
|
7
|
+
* from its own environment or a literal in the recipe (measured on Qwen 0.26.2
|
|
8
|
+
* and Goose 1.54.0). So each runs our bridge (codex-broker-bridge.mjs), told
|
|
9
|
+
* the broker's url, its headers as templates and the path of this spawn's
|
|
10
|
+
* credential file. None of those is a secret. The token stays in that one file,
|
|
11
|
+
* and the bridge reads it for each request.
|
|
12
|
+
*
|
|
13
|
+
* Whether a seat may hold the broker at all was decided before the adapter
|
|
14
|
+
* (grant-broker-guard.js). This decides whether THIS entry can be wired, and a
|
|
15
|
+
* command the entry declares is never run.
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
BRIDGE_HEADERS_VAR, BRIDGE_PATH, BRIDGE_URL_VAR, CREDENTIAL_FILE_VAR,
|
|
19
|
+
} from './codex-broker-bridge.mjs';
|
|
20
|
+
|
|
21
|
+
// A server name becomes a config key or a tool-name prefix.
|
|
22
|
+
export const SERVER_NAME = /^[A-Za-z0-9_-]+$/;
|
|
23
|
+
// What fetch accepts in a header: Latin-1, no CR, LF or NUL.
|
|
24
|
+
// eslint-disable-next-line no-control-regex
|
|
25
|
+
const HEADER_TEXT = /^[^\r\n\0Ā-]*$/;
|
|
26
|
+
const INSTANCE_PLACEHOLDER = /\$\{(?:COMMONLY_API_URL|COMMONLY_INSTANCE_URL)\}/g;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `{ command, args, env, readPath }`, or `{ skip }` with why it can't be wired.
|
|
30
|
+
* `readPath` is the one file a jailed seat must be able to read: the bridge
|
|
31
|
+
* imports node builtins only.
|
|
32
|
+
*/
|
|
33
|
+
export const brokerBridgeEntry = (server, { instanceUrl, credentialFile, runtimeToken } = {}) => {
|
|
34
|
+
if (!SERVER_NAME.test(server?.name || '')) return { skip: 'its name is not a plain config key' };
|
|
35
|
+
if (!credentialFile) return { skip: 'there is no seat credential to read' };
|
|
36
|
+
// Resolve the instance placeholders only: the token placeholder stays in the
|
|
37
|
+
// header template and is filled by the bridge from the file.
|
|
38
|
+
const resolve = (value) => String(value).replace(INSTANCE_PLACEHOLDER, (whole) => instanceUrl || whole);
|
|
39
|
+
const resolvedUrl = resolve(server.url);
|
|
40
|
+
const headers = Object.fromEntries(Object.entries(server.headers || {})
|
|
41
|
+
.map(([name, value]) => [String(name), resolve(String(value))]));
|
|
42
|
+
const carriesToken = (value) => !!runtimeToken && value.includes(runtimeToken);
|
|
43
|
+
if (carriesToken(resolvedUrl)
|
|
44
|
+
|| Object.entries(headers).some(([name, value]) => carriesToken(name) || carriesToken(value))) {
|
|
45
|
+
return { skip: 'the entry holds the seat token as a literal, which would ride a config' };
|
|
46
|
+
}
|
|
47
|
+
if (resolvedUrl.includes('${')) return { skip: 'its url holds a placeholder this host cannot resolve' };
|
|
48
|
+
// A header fetch would reject quotes its value in the error, and that value
|
|
49
|
+
// holds the token once the bridge fills it, so such an entry is not wired.
|
|
50
|
+
if (Object.entries(headers).some(([name, value]) => !HEADER_TEXT.test(name) || !HEADER_TEXT.test(value))) {
|
|
51
|
+
return { skip: 'a header holds a control or non-Latin-1 character' };
|
|
52
|
+
}
|
|
53
|
+
// The url the guard judged (isOurGrantBroker) is normalised: resolved against
|
|
54
|
+
// the instance, with doubled slashes collapsed. The bridge fetches that same
|
|
55
|
+
// url, on the instance's own origin, or nothing.
|
|
56
|
+
let url;
|
|
57
|
+
try {
|
|
58
|
+
const instance = new URL(instanceUrl);
|
|
59
|
+
const parsed = new URL(resolvedUrl, instance);
|
|
60
|
+
if (parsed.origin !== instance.origin) return { skip: 'its url is not on this instance' };
|
|
61
|
+
parsed.pathname = parsed.pathname.replace(/\/{2,}/g, '/');
|
|
62
|
+
url = parsed.href;
|
|
63
|
+
} catch {
|
|
64
|
+
return { skip: 'its url does not resolve against this instance' };
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
command: process.execPath,
|
|
68
|
+
args: [BRIDGE_PATH],
|
|
69
|
+
env: {
|
|
70
|
+
[BRIDGE_URL_VAR]: url,
|
|
71
|
+
[BRIDGE_HEADERS_VAR]: JSON.stringify(headers),
|
|
72
|
+
[CREDENTIAL_FILE_VAR]: credentialFile,
|
|
73
|
+
},
|
|
74
|
+
readPath: BRIDGE_PATH,
|
|
75
|
+
};
|
|
76
|
+
};
|