@guilz-dev/belay 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 +268 -0
- package/agent-belay-logo.png +0 -0
- package/dist/adapters/claude/adapter.d.ts +7 -0
- package/dist/adapters/claude/adapter.js +114 -0
- package/dist/adapters/claude/hooks.d.ts +13 -0
- package/dist/adapters/claude/hooks.js +49 -0
- package/dist/adapters/claude/runtime-entry.d.ts +4 -0
- package/dist/adapters/claude/runtime-entry.js +260 -0
- package/dist/adapters/codex/adapter.d.ts +7 -0
- package/dist/adapters/codex/adapter.js +73 -0
- package/dist/adapters/codex/hooks.d.ts +21 -0
- package/dist/adapters/codex/hooks.js +78 -0
- package/dist/adapters/codex/runtime-entry.d.ts +4 -0
- package/dist/adapters/codex/runtime-entry.js +237 -0
- package/dist/adapters/cursor/adapter.d.ts +7 -0
- package/dist/adapters/cursor/adapter.js +29 -0
- package/dist/adapters/cursor/hooks.d.ts +2 -0
- package/dist/adapters/cursor/hooks.js +26 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
- package/dist/adapters/cursor/runtime-entry.js +143 -0
- package/dist/adapters/layouts/claude.d.ts +2 -0
- package/dist/adapters/layouts/claude.js +40 -0
- package/dist/adapters/layouts/codex.d.ts +2 -0
- package/dist/adapters/layouts/codex.js +43 -0
- package/dist/adapters/layouts/cursor.d.ts +2 -0
- package/dist/adapters/layouts/cursor.js +40 -0
- package/dist/adapters/layouts/index.d.ts +7 -0
- package/dist/adapters/layouts/index.js +23 -0
- package/dist/adapters/layouts/protected-paths.d.ts +3 -0
- package/dist/adapters/layouts/protected-paths.js +15 -0
- package/dist/adapters/layouts/scope.d.ts +19 -0
- package/dist/adapters/layouts/scope.js +86 -0
- package/dist/adapters/layouts/types.d.ts +14 -0
- package/dist/adapters/layouts/types.js +1 -0
- package/dist/adapters/registry.d.ts +4 -0
- package/dist/adapters/registry.js +14 -0
- package/dist/adapters/shared/gate-runtime.d.ts +51 -0
- package/dist/adapters/shared/gate-runtime.js +518 -0
- package/dist/adapters/shared/repo-root.d.ts +2 -0
- package/dist/adapters/shared/repo-root.js +17 -0
- package/dist/adapters/types.d.ts +19 -0
- package/dist/adapters/types.js +1 -0
- package/dist/branding.d.ts +3 -0
- package/dist/branding.js +3 -0
- package/dist/bundle/claude-runtime.mjs +5323 -0
- package/dist/bundle/codex-runtime.mjs +5310 -0
- package/dist/bundle/cursor-runtime.mjs +5208 -0
- package/dist/cleanup-orphans.d.ts +7 -0
- package/dist/cleanup-orphans.js +59 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +631 -0
- package/dist/commands/approve.d.ts +14 -0
- package/dist/commands/approve.js +65 -0
- package/dist/commands/audit.d.ts +59 -0
- package/dist/commands/audit.js +132 -0
- package/dist/commands/classify-for-report.d.ts +9 -0
- package/dist/commands/classify-for-report.js +85 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +366 -0
- package/dist/commands/dogfood.d.ts +5 -0
- package/dist/commands/dogfood.js +71 -0
- package/dist/commands/explain.d.ts +3 -0
- package/dist/commands/explain.js +133 -0
- package/dist/commands/health-snapshot.d.ts +2 -0
- package/dist/commands/health-snapshot.js +166 -0
- package/dist/commands/init-wizard.d.ts +16 -0
- package/dist/commands/init-wizard.js +50 -0
- package/dist/commands/metrics.d.ts +7 -0
- package/dist/commands/metrics.js +89 -0
- package/dist/commands/recover.d.ts +3 -0
- package/dist/commands/recover.js +105 -0
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.js +65 -0
- package/dist/commands/revoke.d.ts +5 -0
- package/dist/commands/revoke.js +22 -0
- package/dist/commands/simulate.d.ts +14 -0
- package/dist/commands/simulate.js +55 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +107 -0
- package/dist/config-io.d.ts +23 -0
- package/dist/config-io.js +180 -0
- package/dist/conformance/guarantee-table.d.ts +14 -0
- package/dist/conformance/guarantee-table.js +95 -0
- package/dist/conformance/types.d.ts +6 -0
- package/dist/conformance/types.js +1 -0
- package/dist/core/approval-service.d.ts +26 -0
- package/dist/core/approval-service.js +41 -0
- package/dist/core/approval-token.d.ts +11 -0
- package/dist/core/approval-token.js +61 -0
- package/dist/core/approval.d.ts +19 -0
- package/dist/core/approval.js +58 -0
- package/dist/core/audit-analysis.d.ts +10 -0
- package/dist/core/audit-analysis.js +147 -0
- package/dist/core/audit-metrics.d.ts +51 -0
- package/dist/core/audit-metrics.js +155 -0
- package/dist/core/audit-query.d.ts +11 -0
- package/dist/core/audit-query.js +142 -0
- package/dist/core/audit-summary.d.ts +33 -0
- package/dist/core/audit-summary.js +111 -0
- package/dist/core/audit-types.d.ts +65 -0
- package/dist/core/audit-types.js +2 -0
- package/dist/core/capability/allowlist.d.ts +10 -0
- package/dist/core/capability/allowlist.js +53 -0
- package/dist/core/capability/broker.d.ts +17 -0
- package/dist/core/capability/broker.js +29 -0
- package/dist/core/capability/index.d.ts +5 -0
- package/dist/core/capability/index.js +4 -0
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +20 -0
- package/dist/core/capability/reasons.d.ts +2 -0
- package/dist/core/capability/reasons.js +4 -0
- package/dist/core/capability/types.d.ts +10 -0
- package/dist/core/capability/types.js +1 -0
- package/dist/core/capability-approval.d.ts +28 -0
- package/dist/core/capability-approval.js +43 -0
- package/dist/core/classify-subagent.d.ts +2 -0
- package/dist/core/classify-subagent.js +69 -0
- package/dist/core/classify-tool.d.ts +3 -0
- package/dist/core/classify-tool.js +311 -0
- package/dist/core/config-layers.d.ts +23 -0
- package/dist/core/config-layers.js +59 -0
- package/dist/core/config.d.ts +219 -0
- package/dist/core/config.js +720 -0
- package/dist/core/control-plane-isolation.d.ts +10 -0
- package/dist/core/control-plane-isolation.js +83 -0
- package/dist/core/custom-command-match.d.ts +2 -0
- package/dist/core/custom-command-match.js +8 -0
- package/dist/core/egress/allowlist.d.ts +7 -0
- package/dist/core/egress/allowlist.js +33 -0
- package/dist/core/egress/env.d.ts +3 -0
- package/dist/core/egress/env.js +17 -0
- package/dist/core/egress/fingerprint.d.ts +3 -0
- package/dist/core/egress/fingerprint.js +35 -0
- package/dist/core/egress/policy.d.ts +8 -0
- package/dist/core/egress/policy.js +47 -0
- package/dist/core/egress/proxy-server.d.ts +21 -0
- package/dist/core/egress/proxy-server.js +263 -0
- package/dist/core/egress/types.d.ts +25 -0
- package/dist/core/egress/types.js +1 -0
- package/dist/core/egress-approval.d.ts +48 -0
- package/dist/core/egress-approval.js +129 -0
- package/dist/core/fingerprint.d.ts +6 -0
- package/dist/core/fingerprint.js +24 -0
- package/dist/core/gate-contract.d.ts +48 -0
- package/dist/core/gate-contract.js +50 -0
- package/dist/core/gate-engine.d.ts +20 -0
- package/dist/core/gate-engine.js +172 -0
- package/dist/core/glob.d.ts +1 -0
- package/dist/core/glob.js +39 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +15 -0
- package/dist/core/integrity.d.ts +15 -0
- package/dist/core/integrity.js +68 -0
- package/dist/core/judge-api-key.d.ts +4 -0
- package/dist/core/judge-api-key.js +11 -0
- package/dist/core/judge-config.d.ts +29 -0
- package/dist/core/judge-config.js +85 -0
- package/dist/core/judge-doctor.d.ts +7 -0
- package/dist/core/judge-doctor.js +124 -0
- package/dist/core/judgment.d.ts +6 -0
- package/dist/core/judgment.js +38 -0
- package/dist/core/notify.d.ts +13 -0
- package/dist/core/notify.js +44 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +107 -0
- package/dist/core/reclassify.d.ts +15 -0
- package/dist/core/reclassify.js +82 -0
- package/dist/core/recover-advice.d.ts +30 -0
- package/dist/core/recover-advice.js +177 -0
- package/dist/core/recover-git-probe.d.ts +8 -0
- package/dist/core/recover-git-probe.js +50 -0
- package/dist/core/recover-select.d.ts +10 -0
- package/dist/core/recover-select.js +60 -0
- package/dist/core/scrub.d.ts +3 -0
- package/dist/core/scrub.js +87 -0
- package/dist/core/shell-substitution.d.ts +6 -0
- package/dist/core/shell-substitution.js +130 -0
- package/dist/core/shell-tokenizer.d.ts +5 -0
- package/dist/core/shell-tokenizer.js +129 -0
- package/dist/core/shell-unparseable.d.ts +4 -0
- package/dist/core/shell-unparseable.js +96 -0
- package/dist/core/transactional/diff-evaluator.d.ts +2 -0
- package/dist/core/transactional/diff-evaluator.js +84 -0
- package/dist/core/transactional/eligibility.d.ts +4 -0
- package/dist/core/transactional/eligibility.js +44 -0
- package/dist/core/transactional/git-worktree.d.ts +13 -0
- package/dist/core/transactional/git-worktree.js +189 -0
- package/dist/core/transactional/index.d.ts +5 -0
- package/dist/core/transactional/index.js +4 -0
- package/dist/core/transactional/reasons.d.ts +4 -0
- package/dist/core/transactional/reasons.js +8 -0
- package/dist/core/transactional/runner.d.ts +2 -0
- package/dist/core/transactional/runner.js +113 -0
- package/dist/core/transactional/types.d.ts +46 -0
- package/dist/core/transactional/types.js +1 -0
- package/dist/core/types.d.ts +90 -0
- package/dist/core/types.js +1 -0
- package/dist/core/v2/adapter.d.ts +14 -0
- package/dist/core/v2/adapter.js +118 -0
- package/dist/core/v2/containment.d.ts +19 -0
- package/dist/core/v2/containment.js +91 -0
- package/dist/core/v2/egress-classify.d.ts +7 -0
- package/dist/core/v2/egress-classify.js +216 -0
- package/dist/core/v2/fingerprint.d.ts +1 -0
- package/dist/core/v2/fingerprint.js +4 -0
- package/dist/core/v2/index.d.ts +12 -0
- package/dist/core/v2/index.js +10 -0
- package/dist/core/v2/judge-audit.d.ts +2 -0
- package/dist/core/v2/judge-audit.js +15 -0
- package/dist/core/v2/judge-factory.d.ts +25 -0
- package/dist/core/v2/judge-factory.js +75 -0
- package/dist/core/v2/judge-outbound.d.ts +12 -0
- package/dist/core/v2/judge-outbound.js +73 -0
- package/dist/core/v2/judge.d.ts +47 -0
- package/dist/core/v2/judge.js +264 -0
- package/dist/core/v2/launcher-resolve.d.ts +12 -0
- package/dist/core/v2/launcher-resolve.js +190 -0
- package/dist/core/v2/overrides.d.ts +7 -0
- package/dist/core/v2/overrides.js +37 -0
- package/dist/core/v2/parser.d.ts +21 -0
- package/dist/core/v2/parser.js +213 -0
- package/dist/core/v2/types.d.ts +67 -0
- package/dist/core/v2/types.js +1 -0
- package/dist/core/v2/verdict.d.ts +2 -0
- package/dist/core/v2/verdict.js +699 -0
- package/dist/corpus/evaluate.d.ts +24 -0
- package/dist/corpus/evaluate.js +69 -0
- package/dist/defaults.d.ts +18 -0
- package/dist/defaults.js +155 -0
- package/dist/egress-daemon.d.ts +1 -0
- package/dist/egress-daemon.js +52 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/installer/bootstrap.d.ts +5 -0
- package/dist/installer/bootstrap.js +61 -0
- package/dist/installer/runtime-artifacts.d.ts +3 -0
- package/dist/installer/runtime-artifacts.js +23 -0
- package/dist/installer/scope-config.d.ts +8 -0
- package/dist/installer/scope-config.js +25 -0
- package/dist/installer.d.ts +22 -0
- package/dist/installer.js +169 -0
- package/dist/node-resolution.d.ts +8 -0
- package/dist/node-resolution.js +237 -0
- package/dist/operational-insights.d.ts +19 -0
- package/dist/operational-insights.js +24 -0
- package/dist/presets.d.ts +4 -0
- package/dist/presets.js +95 -0
- package/dist/services/egress-service.d.ts +57 -0
- package/dist/services/egress-service.js +334 -0
- package/dist/services/sandbox-service.d.ts +38 -0
- package/dist/services/sandbox-service.js +95 -0
- package/dist/templates.d.ts +7 -0
- package/dist/templates.js +56 -0
- package/dist/types.d.ts +230 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +65 -0
- package/skills/belay/SKILL.md +52 -0
- package/skills/belay/belay-approve.md +7 -0
- package/skills/belay/belay-explain.md +11 -0
- package/skills/belay/belay-recover.md +13 -0
- package/skills/belay/belay-report.md +7 -0
- package/skills/belay/belay-status.md +9 -0
- package/skills/belay/belay-why.md +11 -0
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
4
|
+
import net from 'node:net';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { approvedApprovalsPath, belayStateDir, loadApprovalState, loadConfigFile, pendingApprovalsPath, repoLocalStateDirFor, } from '../config-io.js';
|
|
8
|
+
import { configuredControlPlaneDir } from '../core/config.js';
|
|
9
|
+
import { egressAllowlistPath } from '../core/egress/allowlist.js';
|
|
10
|
+
import { formatProxyEnv, recommendedProxyEnv } from '../core/egress/env.js';
|
|
11
|
+
function egressStatePaths(repoRoot, config) {
|
|
12
|
+
const stateDir = belayStateDir(config, repoLocalStateDirFor(repoRoot, config));
|
|
13
|
+
return {
|
|
14
|
+
stateDir,
|
|
15
|
+
pidPath: path.join(stateDir, 'egress-proxy.pid'),
|
|
16
|
+
statusPath: path.join(stateDir, 'egress-proxy.json'),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function daemonScriptPath() {
|
|
20
|
+
return fileURLToPath(new URL('../egress-daemon.js', import.meta.url));
|
|
21
|
+
}
|
|
22
|
+
async function readStatusFile(statusPath) {
|
|
23
|
+
if (!existsSync(statusPath)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
try {
|
|
27
|
+
const raw = JSON.parse(await readFile(statusPath, 'utf8'));
|
|
28
|
+
if (typeof raw.pid !== 'number') {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
pid: raw.pid,
|
|
33
|
+
host: raw.host ?? '127.0.0.1',
|
|
34
|
+
port: raw.port ?? 17831,
|
|
35
|
+
startedAt: raw.startedAt ?? '',
|
|
36
|
+
repoRoot: typeof raw.repoRoot === 'string' ? raw.repoRoot : undefined,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function isPortOpen(host, port) {
|
|
44
|
+
return new Promise((resolve) => {
|
|
45
|
+
const socket = net.createConnection({ host, port });
|
|
46
|
+
const finish = (open) => {
|
|
47
|
+
socket.destroy();
|
|
48
|
+
resolve(open);
|
|
49
|
+
};
|
|
50
|
+
socket.setTimeout(300);
|
|
51
|
+
socket.on('connect', () => finish(true));
|
|
52
|
+
socket.on('timeout', () => finish(false));
|
|
53
|
+
socket.on('error', () => finish(false));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function resolveLiveEgressStatus(repoRoot, config) {
|
|
57
|
+
const { statusPath } = egressStatePaths(repoRoot, config);
|
|
58
|
+
const statusCandidates = [statusPath];
|
|
59
|
+
const controlPlaneStatus = path.join(configuredControlPlaneDir(config), 'egress-proxy.json');
|
|
60
|
+
if (!statusCandidates.includes(controlPlaneStatus)) {
|
|
61
|
+
statusCandidates.push(controlPlaneStatus);
|
|
62
|
+
}
|
|
63
|
+
let status = null;
|
|
64
|
+
for (const candidate of statusCandidates) {
|
|
65
|
+
const candidateStatus = await readStatusFile(candidate);
|
|
66
|
+
if (candidateStatus && isProcessAlive(candidateStatus.pid)) {
|
|
67
|
+
status = candidateStatus;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const host = status?.host ?? config.egress.listenHost;
|
|
72
|
+
const port = status?.port ?? config.egress.listenPort;
|
|
73
|
+
const portOccupied = await isPortOpen(host, port);
|
|
74
|
+
return { status, host, port, portOccupied };
|
|
75
|
+
}
|
|
76
|
+
async function waitForEgressRunning(repoRoot, timeoutMs = 5000) {
|
|
77
|
+
const deadline = Date.now() + timeoutMs;
|
|
78
|
+
while (Date.now() < deadline) {
|
|
79
|
+
const status = await egressStatus({ targetDir: repoRoot });
|
|
80
|
+
if (status.running && !status.foreignProxy) {
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
84
|
+
}
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
export function isEgressProxyActiveForRepo(config, repoRoot, repoLocalStateDir) {
|
|
88
|
+
if (!config.egress.enabled || !config.egress.demoteL3External) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const stateDirs = new Set([
|
|
92
|
+
belayStateDir(config, repoLocalStateDir),
|
|
93
|
+
configuredControlPlaneDir(config),
|
|
94
|
+
]);
|
|
95
|
+
const resolvedRepoRoot = path.resolve(repoRoot);
|
|
96
|
+
for (const stateDir of stateDirs) {
|
|
97
|
+
const statusPath = path.join(stateDir, 'egress-proxy.json');
|
|
98
|
+
if (!existsSync(statusPath)) {
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const raw = JSON.parse(readFileSync(statusPath, 'utf8'));
|
|
103
|
+
if (typeof raw.pid !== 'number' || !isProcessAlive(raw.pid)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (raw.repoRoot && path.resolve(raw.repoRoot) !== resolvedRepoRoot) {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
catch { }
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
function isProcessAlive(pid) {
|
|
116
|
+
try {
|
|
117
|
+
process.kill(pid, 0);
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
export async function egressStatus(options = {}) {
|
|
125
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
126
|
+
const config = await loadConfigFile(repoRoot);
|
|
127
|
+
const { status, host, port, portOccupied } = await resolveLiveEgressStatus(repoRoot, config);
|
|
128
|
+
const ownedRunning = Boolean(status);
|
|
129
|
+
const running = ownedRunning || portOccupied;
|
|
130
|
+
const boundRepoRoot = status?.repoRoot ?? null;
|
|
131
|
+
const foreignProxy = portOccupied && !ownedRunning;
|
|
132
|
+
const repoRootMismatch = Boolean((boundRepoRoot && boundRepoRoot !== repoRoot) || foreignProxy);
|
|
133
|
+
return {
|
|
134
|
+
repoRoot,
|
|
135
|
+
enabled: config.egress.enabled,
|
|
136
|
+
running,
|
|
137
|
+
host,
|
|
138
|
+
port,
|
|
139
|
+
pid: ownedRunning ? (status?.pid ?? null) : null,
|
|
140
|
+
startedAt: ownedRunning ? (status?.startedAt ?? null) : null,
|
|
141
|
+
boundRepoRoot: foreignProxy ? boundRepoRoot : boundRepoRoot,
|
|
142
|
+
repoRootMismatch,
|
|
143
|
+
foreignProxy,
|
|
144
|
+
portOccupied,
|
|
145
|
+
proxyEnv: recommendedProxyEnv(config.egress),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
export async function startEgressProxy(options = {}) {
|
|
149
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
150
|
+
const config = await loadConfigFile(repoRoot);
|
|
151
|
+
if (!config.egress.enabled) {
|
|
152
|
+
return {
|
|
153
|
+
ok: false,
|
|
154
|
+
message: 'Egress proxy is disabled in config. Set egress.enabled to true first.',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const current = await egressStatus({ targetDir: repoRoot });
|
|
158
|
+
if (current.foreignProxy) {
|
|
159
|
+
return {
|
|
160
|
+
ok: false,
|
|
161
|
+
message: `Port ${current.host}:${current.port} is already in use by another egress proxy${current.boundRepoRoot ? ` for ${current.boundRepoRoot}` : ''}. Stop it before starting for ${repoRoot}.`,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (current.running) {
|
|
165
|
+
if (current.repoRootMismatch) {
|
|
166
|
+
return {
|
|
167
|
+
ok: false,
|
|
168
|
+
message: `Egress proxy already running for ${current.boundRepoRoot} (pid ${current.pid}). Stop it before starting for ${repoRoot}.`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
ok: true,
|
|
173
|
+
message: `Egress proxy already running (pid ${current.pid}) at ${current.host}:${current.port} for ${current.boundRepoRoot ?? repoRoot}.`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const { stateDir } = egressStatePaths(repoRoot, config);
|
|
177
|
+
await mkdir(stateDir, { recursive: true });
|
|
178
|
+
const child = spawn(process.execPath, [daemonScriptPath()], {
|
|
179
|
+
detached: true,
|
|
180
|
+
stdio: 'ignore',
|
|
181
|
+
env: {
|
|
182
|
+
...process.env,
|
|
183
|
+
BELAY_EGRESS_REPO_ROOT: repoRoot,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
child.unref();
|
|
187
|
+
const started = await waitForEgressRunning(repoRoot);
|
|
188
|
+
const after = await egressStatus({ targetDir: repoRoot });
|
|
189
|
+
if (!started || !after.running || after.foreignProxy) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
message: 'Failed to start egress proxy. Check that the listen port is free.',
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
message: `Egress proxy started (pid ${after.pid}) at ${after.host}:${after.port}.`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
export async function stopEgressProxy(options = {}) {
|
|
201
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
202
|
+
const config = await loadConfigFile(repoRoot);
|
|
203
|
+
const { pidPath, statusPath } = egressStatePaths(repoRoot, config);
|
|
204
|
+
const status = await readStatusFile(statusPath);
|
|
205
|
+
if (!status || !isProcessAlive(status.pid)) {
|
|
206
|
+
if (existsSync(pidPath)) {
|
|
207
|
+
await unlink(pidPath).catch(() => undefined);
|
|
208
|
+
}
|
|
209
|
+
if (existsSync(statusPath)) {
|
|
210
|
+
await unlink(statusPath).catch(() => undefined);
|
|
211
|
+
}
|
|
212
|
+
return { ok: true, message: 'Egress proxy is not running.' };
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
process.kill(status.pid, 'SIGTERM');
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
return { ok: false, message: `Failed to stop egress proxy (pid ${status.pid}).` };
|
|
219
|
+
}
|
|
220
|
+
await unlink(pidPath).catch(() => undefined);
|
|
221
|
+
await unlink(statusPath).catch(() => undefined);
|
|
222
|
+
return { ok: true, message: `Stopped egress proxy (pid ${status.pid}).` };
|
|
223
|
+
}
|
|
224
|
+
export async function egressEnv(options = {}) {
|
|
225
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
226
|
+
const config = await loadConfigFile(repoRoot);
|
|
227
|
+
if (!config.egress.enabled) {
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
message: 'Egress proxy is disabled in config.',
|
|
231
|
+
env: {},
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
const status = await egressStatus({ targetDir: repoRoot });
|
|
235
|
+
if (status.foreignProxy) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
message: `Port ${status.host}:${status.port} is in use by another egress proxy${status.boundRepoRoot ? ` for ${status.boundRepoRoot}` : ''}. Do not export proxy env for ${repoRoot} until it is stopped.`,
|
|
239
|
+
env: {},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
if (status.repoRootMismatch) {
|
|
243
|
+
return {
|
|
244
|
+
ok: false,
|
|
245
|
+
message: `Egress proxy is bound to ${status.boundRepoRoot}, not ${repoRoot}. Stop and restart egress for this repository.`,
|
|
246
|
+
env: {},
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
if (!status.running) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
message: 'Egress proxy is not running. Run belay egress start first.',
|
|
253
|
+
env: {},
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const env = recommendedProxyEnv(config.egress);
|
|
257
|
+
return {
|
|
258
|
+
ok: true,
|
|
259
|
+
message: formatProxyEnv(config.egress),
|
|
260
|
+
env,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export function formatEgressStatusReport(report) {
|
|
264
|
+
const lines = [
|
|
265
|
+
`belay egress status for ${report.repoRoot}`,
|
|
266
|
+
`Config enabled: ${report.enabled ? 'yes' : 'no'}`,
|
|
267
|
+
`Running: ${report.running ? 'yes' : 'no'}`,
|
|
268
|
+
`Listen: ${report.host}:${report.port}`,
|
|
269
|
+
];
|
|
270
|
+
if (report.pid) {
|
|
271
|
+
lines.push(`PID: ${report.pid}`);
|
|
272
|
+
}
|
|
273
|
+
if (report.startedAt) {
|
|
274
|
+
lines.push(`Started: ${report.startedAt}`);
|
|
275
|
+
}
|
|
276
|
+
if (report.boundRepoRoot) {
|
|
277
|
+
lines.push(`Bound repo: ${report.boundRepoRoot}`);
|
|
278
|
+
}
|
|
279
|
+
if (report.foreignProxy) {
|
|
280
|
+
lines.push('Warning: listen port is occupied by a proxy not owned by this repository state.');
|
|
281
|
+
}
|
|
282
|
+
if (report.repoRootMismatch) {
|
|
283
|
+
lines.push(`Warning: proxy is bound to a different repository than ${report.repoRoot}.`);
|
|
284
|
+
}
|
|
285
|
+
lines.push('', 'Recommended proxy environment:');
|
|
286
|
+
for (const [key, value] of Object.entries(report.proxyEnv)) {
|
|
287
|
+
lines.push(` ${key}=${value}`);
|
|
288
|
+
}
|
|
289
|
+
return `${lines.join('\n')}\n`;
|
|
290
|
+
}
|
|
291
|
+
export function createEgressApprovalStore(repoRoot, config) {
|
|
292
|
+
const repoLocalDir = repoLocalStateDirFor(repoRoot, config);
|
|
293
|
+
return {
|
|
294
|
+
allowlistPath: egressAllowlistPath(config, repoLocalDir),
|
|
295
|
+
async loadPending() {
|
|
296
|
+
const filePath = pendingApprovalsPath(repoRoot, config);
|
|
297
|
+
return {
|
|
298
|
+
filePath,
|
|
299
|
+
state: await loadApprovalState(repoRoot, 'pending-approvals.json', config),
|
|
300
|
+
};
|
|
301
|
+
},
|
|
302
|
+
async loadApproved() {
|
|
303
|
+
const filePath = approvedApprovalsPath(repoRoot, config);
|
|
304
|
+
return {
|
|
305
|
+
filePath,
|
|
306
|
+
state: await loadApprovalState(repoRoot, 'approved-approvals.json', config),
|
|
307
|
+
};
|
|
308
|
+
},
|
|
309
|
+
async writePending(_filePath, state) {
|
|
310
|
+
const { saveApprovalState } = await import('../config-io.js');
|
|
311
|
+
await saveApprovalState(repoRoot, 'pending-approvals.json', state, config);
|
|
312
|
+
},
|
|
313
|
+
async writeApproved(_filePath, state) {
|
|
314
|
+
const { saveApprovalState } = await import('../config-io.js');
|
|
315
|
+
await saveApprovalState(repoRoot, 'approved-approvals.json', state, config);
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
export async function writeEgressDaemonState(params) {
|
|
320
|
+
await mkdir(params.stateDir, { recursive: true });
|
|
321
|
+
const startedAt = new Date().toISOString();
|
|
322
|
+
await writeFile(path.join(params.stateDir, 'egress-proxy.pid'), `${params.pid}\n`, 'utf8');
|
|
323
|
+
await writeFile(path.join(params.stateDir, 'egress-proxy.json'), `${JSON.stringify({
|
|
324
|
+
pid: params.pid,
|
|
325
|
+
host: params.host,
|
|
326
|
+
port: params.port,
|
|
327
|
+
startedAt,
|
|
328
|
+
repoRoot: params.repoRoot,
|
|
329
|
+
}, null, 2)}\n`, 'utf8');
|
|
330
|
+
}
|
|
331
|
+
export async function clearEgressDaemonState(stateDir) {
|
|
332
|
+
await unlink(path.join(stateDir, 'egress-proxy.pid')).catch(() => undefined);
|
|
333
|
+
await unlink(path.join(stateDir, 'egress-proxy.json')).catch(() => undefined);
|
|
334
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { loadConfigFile } from '../config-io.js';
|
|
2
|
+
export interface SandboxServiceOptions {
|
|
3
|
+
targetDir?: string;
|
|
4
|
+
}
|
|
5
|
+
export interface SandboxStatusReport {
|
|
6
|
+
repoRoot: string;
|
|
7
|
+
sandboxEnabled: boolean;
|
|
8
|
+
sandboxRuntime: string;
|
|
9
|
+
denyNetworkByDefault: boolean;
|
|
10
|
+
brokerActive: boolean;
|
|
11
|
+
fsScopeAllowlistCount: number;
|
|
12
|
+
controlPlaneIsolationMode: string;
|
|
13
|
+
controlPlaneIsolationOk: boolean;
|
|
14
|
+
l1FullActive: boolean;
|
|
15
|
+
l1Full: {
|
|
16
|
+
sandbox: boolean;
|
|
17
|
+
egress: boolean;
|
|
18
|
+
egressProxyRunning: boolean;
|
|
19
|
+
controlPlaneIsolation: boolean;
|
|
20
|
+
approvalSigningRequired: boolean;
|
|
21
|
+
};
|
|
22
|
+
issues: string[];
|
|
23
|
+
}
|
|
24
|
+
export declare function sandboxStatus(options?: SandboxServiceOptions): Promise<SandboxStatusReport>;
|
|
25
|
+
export declare function createCapabilityApprovalStore(repoRoot: string, config: Awaited<ReturnType<typeof loadConfigFile>>): {
|
|
26
|
+
allowlistPath: string;
|
|
27
|
+
loadPending(): Promise<{
|
|
28
|
+
filePath: string;
|
|
29
|
+
state: import("../types.js").ApprovalStateFile;
|
|
30
|
+
}>;
|
|
31
|
+
loadApproved(): Promise<{
|
|
32
|
+
filePath: string;
|
|
33
|
+
state: import("../types.js").ApprovalStateFile;
|
|
34
|
+
}>;
|
|
35
|
+
writePending(_filePath: string, state: import("../core/types.js").ApprovalStateFile): Promise<void>;
|
|
36
|
+
writeApproved(_filePath: string, state: import("../core/types.js").ApprovalStateFile): Promise<void>;
|
|
37
|
+
};
|
|
38
|
+
export declare function formatSandboxStatusReport(report: SandboxStatusReport): string;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { loadConfigFile, repoLocalStateDirFor } from '../config-io.js';
|
|
3
|
+
import { fsScopeAllowlistPath, loadFsScopeAllowlist } from '../core/capability/allowlist.js';
|
|
4
|
+
import { evaluateL1FullStatus, isCapabilityBrokerDemotionActive, } from '../core/capability/broker.js';
|
|
5
|
+
import { configuredControlPlaneDir } from '../core/config.js';
|
|
6
|
+
import { verifyControlPlaneIsolation } from '../core/control-plane-isolation.js';
|
|
7
|
+
import { egressStatus } from './egress-service.js';
|
|
8
|
+
export async function sandboxStatus(options = {}) {
|
|
9
|
+
const repoRoot = path.resolve(options.targetDir ?? process.cwd());
|
|
10
|
+
const config = await loadConfigFile(repoRoot);
|
|
11
|
+
const repoLocalStateDir = repoLocalStateDirFor(repoRoot, config);
|
|
12
|
+
const allowlistPath = fsScopeAllowlistPath(config, repoLocalStateDir);
|
|
13
|
+
const allowlist = await loadFsScopeAllowlist(allowlistPath);
|
|
14
|
+
const egress = await egressStatus({ targetDir: repoRoot });
|
|
15
|
+
const isolation = verifyControlPlaneIsolation(configuredControlPlaneDir(config), config.controlPlane.isolation);
|
|
16
|
+
const l1Full = evaluateL1FullStatus({
|
|
17
|
+
config,
|
|
18
|
+
egressProxyRunning: egress.running && !egress.foreignProxy && !egress.repoRootMismatch,
|
|
19
|
+
});
|
|
20
|
+
const issues = [...isolation.issues];
|
|
21
|
+
if (config.sandbox.enabled && config.sandbox.runtime === 'none') {
|
|
22
|
+
issues.push('sandbox.enabled is true but sandbox.runtime is none');
|
|
23
|
+
}
|
|
24
|
+
if (l1Full.sandbox && l1Full.egress && !l1Full.egressProxyRunning) {
|
|
25
|
+
issues.push('L1-full requires a running egress proxy for this repository');
|
|
26
|
+
}
|
|
27
|
+
if (l1Full.sandbox && !l1Full.controlPlaneIsolation) {
|
|
28
|
+
issues.push('L1-full requires controlPlane.isolation mode other than none');
|
|
29
|
+
}
|
|
30
|
+
if (l1Full.sandbox && !l1Full.approvalSigningRequired) {
|
|
31
|
+
issues.push('L1-full requires approvalSigning.required=true');
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
repoRoot,
|
|
35
|
+
sandboxEnabled: config.sandbox.enabled,
|
|
36
|
+
sandboxRuntime: config.sandbox.runtime,
|
|
37
|
+
denyNetworkByDefault: config.sandbox.denyNetworkByDefault,
|
|
38
|
+
brokerActive: isCapabilityBrokerDemotionActive(config),
|
|
39
|
+
fsScopeAllowlistCount: allowlist.paths.length,
|
|
40
|
+
controlPlaneIsolationMode: config.controlPlane.isolation.mode,
|
|
41
|
+
controlPlaneIsolationOk: isolation.ok,
|
|
42
|
+
l1FullActive: l1Full.active,
|
|
43
|
+
l1Full,
|
|
44
|
+
issues,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
export function createCapabilityApprovalStore(repoRoot, config) {
|
|
48
|
+
const repoLocalDir = repoLocalStateDirFor(repoRoot, config);
|
|
49
|
+
return {
|
|
50
|
+
allowlistPath: fsScopeAllowlistPath(config, repoLocalDir),
|
|
51
|
+
async loadPending() {
|
|
52
|
+
const { loadApprovalState, pendingApprovalsPath } = await import('../config-io.js');
|
|
53
|
+
const filePath = pendingApprovalsPath(repoRoot, config);
|
|
54
|
+
return {
|
|
55
|
+
filePath,
|
|
56
|
+
state: await loadApprovalState(repoRoot, 'pending-approvals.json', config),
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
async loadApproved() {
|
|
60
|
+
const { loadApprovalState, approvedApprovalsPath } = await import('../config-io.js');
|
|
61
|
+
const filePath = approvedApprovalsPath(repoRoot, config);
|
|
62
|
+
return {
|
|
63
|
+
filePath,
|
|
64
|
+
state: await loadApprovalState(repoRoot, 'approved-approvals.json', config),
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
async writePending(_filePath, state) {
|
|
68
|
+
const { saveApprovalState } = await import('../config-io.js');
|
|
69
|
+
await saveApprovalState(repoRoot, 'pending-approvals.json', state, config);
|
|
70
|
+
},
|
|
71
|
+
async writeApproved(_filePath, state) {
|
|
72
|
+
const { saveApprovalState } = await import('../config-io.js');
|
|
73
|
+
await saveApprovalState(repoRoot, 'approved-approvals.json', state, config);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
export function formatSandboxStatusReport(report) {
|
|
78
|
+
const lines = [
|
|
79
|
+
`belay sandbox status for ${report.repoRoot}`,
|
|
80
|
+
`Sandbox: ${report.sandboxEnabled ? 'enabled' : 'disabled'} (runtime=${report.sandboxRuntime})`,
|
|
81
|
+
`Capability broker (fs-scope): ${report.brokerActive ? 'active' : 'inactive'}`,
|
|
82
|
+
`FS-scope allowlist entries: ${report.fsScopeAllowlistCount}`,
|
|
83
|
+
`Control-plane isolation: ${report.controlPlaneIsolationMode} (ok=${report.controlPlaneIsolationOk})`,
|
|
84
|
+
`L1-full active: ${report.l1FullActive}`,
|
|
85
|
+
` sandbox=${report.l1Full.sandbox} egress=${report.l1Full.egress} proxy=${report.l1Full.egressProxyRunning}`,
|
|
86
|
+
` isolation=${report.l1Full.controlPlaneIsolation} signing=${report.l1Full.approvalSigningRequired}`,
|
|
87
|
+
];
|
|
88
|
+
if (report.issues.length > 0) {
|
|
89
|
+
lines.push('Issues:');
|
|
90
|
+
for (const issue of report.issues) {
|
|
91
|
+
lines.push(` - ${issue}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return `${lines.join('\n')}\n`;
|
|
95
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { BelayConfigV3 } from './core/config.js';
|
|
2
|
+
export declare function renderConfig(config: BelayConfigV3): string;
|
|
3
|
+
export declare function renderBeforeSubmitHook(): string;
|
|
4
|
+
export declare function renderShellGateHook(): string;
|
|
5
|
+
export declare function renderToolGateHook(): string;
|
|
6
|
+
export declare function renderAuditHook(): string;
|
|
7
|
+
export declare function renderRuntimeCore(adapter?: 'cursor' | 'claude' | 'codex'): Promise<string>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { PACKAGE_VERSION } from './version.js';
|
|
5
|
+
function inlineJson(value) {
|
|
6
|
+
return JSON.stringify(value, null, 2);
|
|
7
|
+
}
|
|
8
|
+
export function renderConfig(config) {
|
|
9
|
+
return `${inlineJson(config)}\n`;
|
|
10
|
+
}
|
|
11
|
+
export function renderBeforeSubmitHook() {
|
|
12
|
+
return `import { runBeforeSubmitPromptHook } from '../belay/runtime/core.mjs'
|
|
13
|
+
|
|
14
|
+
await runBeforeSubmitPromptHook()
|
|
15
|
+
`;
|
|
16
|
+
}
|
|
17
|
+
export function renderShellGateHook() {
|
|
18
|
+
return `import { runShellGateHook } from '../belay/runtime/core.mjs'
|
|
19
|
+
|
|
20
|
+
await runShellGateHook()
|
|
21
|
+
`;
|
|
22
|
+
}
|
|
23
|
+
export function renderToolGateHook() {
|
|
24
|
+
return `import { runToolGateHook } from '../belay/runtime/core.mjs'
|
|
25
|
+
|
|
26
|
+
const eventName = process.argv[2] ?? 'preToolUse'
|
|
27
|
+
await runToolGateHook(eventName)
|
|
28
|
+
`;
|
|
29
|
+
}
|
|
30
|
+
export function renderAuditHook() {
|
|
31
|
+
return `import { runAuditHook } from '../belay/runtime/core.mjs'
|
|
32
|
+
|
|
33
|
+
const eventName = process.argv[2] ?? 'postToolUse'
|
|
34
|
+
await runAuditHook(eventName)
|
|
35
|
+
`;
|
|
36
|
+
}
|
|
37
|
+
async function readRuntimeBundle(adapter = 'cursor') {
|
|
38
|
+
const bundlePath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'dist', 'bundle', `${adapter}-runtime.mjs`);
|
|
39
|
+
try {
|
|
40
|
+
return await readFile(bundlePath, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
throw new Error('Runtime bundle missing. Run pnpm build before belay init or upgrade.');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function renderRuntimeCore(adapter = 'cursor') {
|
|
47
|
+
const bundle = await readRuntimeBundle(adapter);
|
|
48
|
+
const stamp = `export const RUNTIME_BUILD_STAMP = ${JSON.stringify(`${PACKAGE_VERSION}@${new Date().toISOString()}`)};\n`;
|
|
49
|
+
const versionLine = `export const RUNTIME_PACKAGE_VERSION = ${JSON.stringify(PACKAGE_VERSION)};\n`;
|
|
50
|
+
const withoutStamp = bundle
|
|
51
|
+
.replace(/^export const RUNTIME_BUILD_STAMP = .*;\n/gm, '')
|
|
52
|
+
.replace(/^export const RUNTIME_PACKAGE_VERSION = .*;\n/gm, '')
|
|
53
|
+
.replace(/^var RUNTIME_PACKAGE_VERSION = .*;\n/gm, '')
|
|
54
|
+
.replace(/\n {2}RUNTIME_PACKAGE_VERSION,\n/, '\n');
|
|
55
|
+
return `${versionLine}${stamp}${withoutStamp}`;
|
|
56
|
+
}
|