@superjackfruitlabs/salaam 0.0.1 → 0.0.2
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/CHANGELOG.md +15 -0
- package/README.md +25 -17
- package/dist/src/a2a-backend.js +225 -0
- package/dist/src/a2a-inbound.js +487 -0
- package/dist/src/a2a-peer.js +370 -0
- package/dist/src/a2a-protocol.js +250 -0
- package/dist/src/agent-profiles.js +289 -0
- package/dist/src/children.js +132 -26
- package/dist/src/claude-provider.js +2 -179
- package/dist/src/cli-help.js +107 -6
- package/dist/src/cli.js +477 -13
- package/dist/src/config.js +26 -3
- package/dist/src/credentials.js +7 -2
- package/dist/src/external-runtime.js +1 -0
- package/dist/src/external-work.js +583 -0
- package/dist/src/harness-backend.js +466 -0
- package/dist/src/harness-container.js +450 -0
- package/dist/src/harness-model-gateway.js +104 -0
- package/dist/src/harness-prepare.js +88 -0
- package/dist/src/harness-probe.js +111 -0
- package/dist/src/harness-protocol.js +325 -0
- package/dist/src/harness-readiness.js +50 -0
- package/dist/src/harness-rpc.js +206 -0
- package/dist/src/harness-tool-identity.js +87 -0
- package/dist/src/harness-wire-bridge.js +387 -0
- package/dist/src/host.js +6 -1
- package/dist/src/house-extension.js +147 -64
- package/dist/src/live-provider.js +301 -0
- package/dist/src/managed.js +33 -3
- package/dist/src/protocol.js +9 -5
- package/dist/src/provider-auth.js +300 -0
- package/dist/src/provider-config.js +65 -17
- package/dist/src/provider-model.js +78 -0
- package/dist/src/provider-payload.js +264 -0
- package/dist/src/provider-transport.js +36 -0
- package/dist/src/provider-usage.js +59 -0
- package/dist/src/repository-snapshots.js +219 -0
- package/dist/src/service.js +274 -9
- package/dist/src/store.js +44 -2
- package/dist/src/supervisor.js +246 -35
- package/dist/src/task-budget.js +17 -8
- package/dist/src/task-context.js +15 -0
- package/dist/src/task-driver.js +38 -14
- package/dist/src/task-policy.js +20 -1
- package/dist/src/task-verification.js +3 -0
- package/dist/src/task-work.js +31 -0
- package/dist/src/team-work.js +378 -0
- package/dist/src/tui-view.js +65 -10
- package/dist/src/tui.js +4 -2
- package/dist/src/usage.js +18 -0
- package/dist/src/worker-boundary.js +298 -0
- package/dist/src/worker-workspaces.js +274 -0
- package/dist/src/workspaces.js +6 -2
- package/harness-runtime/codex-pins.json +10 -0
- package/harness-runtime/package-lock.json +560 -0
- package/harness-runtime/package.json +9 -0
- package/npm-shrinkwrap.json +57 -2
- package/package.json +2 -1
- package/src/a2a-backend.ts +254 -0
- package/src/a2a-inbound.ts +585 -0
- package/src/a2a-peer.ts +441 -0
- package/src/a2a-protocol.ts +286 -0
- package/src/agent-profiles.ts +379 -0
- package/src/children.ts +168 -27
- package/src/claude-provider.ts +2 -189
- package/src/cli-help.ts +107 -6
- package/src/cli.ts +602 -12
- package/src/config.ts +34 -3
- package/src/credentials.ts +9 -2
- package/src/external-runtime.ts +54 -0
- package/src/external-work.ts +663 -0
- package/src/harness-backend.ts +557 -0
- package/src/harness-container.ts +543 -0
- package/src/harness-model-gateway.ts +138 -0
- package/src/harness-prepare.ts +113 -0
- package/src/harness-probe.ts +150 -0
- package/src/harness-protocol.ts +388 -0
- package/src/harness-readiness.ts +71 -0
- package/src/harness-rpc.ts +244 -0
- package/src/harness-tool-identity.ts +91 -0
- package/src/harness-wire-bridge.ts +418 -0
- package/src/host.ts +11 -6
- package/src/house-extension.ts +165 -66
- package/src/live-provider.ts +347 -0
- package/src/managed.ts +43 -4
- package/src/protocol.ts +36 -8
- package/src/provider-auth.ts +336 -0
- package/src/provider-config.ts +89 -18
- package/src/provider-model.ts +86 -0
- package/src/provider-payload.ts +263 -0
- package/src/provider-transport.ts +51 -0
- package/src/provider-usage.ts +64 -0
- package/src/repository-snapshots.ts +259 -0
- package/src/service.ts +446 -11
- package/src/store.ts +60 -4
- package/src/supervisor.ts +294 -41
- package/src/task-budget.ts +24 -11
- package/src/task-context.ts +17 -0
- package/src/task-driver.ts +51 -15
- package/src/task-policy.ts +23 -3
- package/src/task-verification.ts +2 -0
- package/src/task-work.ts +46 -1
- package/src/team-work.ts +554 -0
- package/src/tui-client.ts +5 -0
- package/src/tui-view.ts +164 -12
- package/src/tui.ts +5 -2
- package/src/usage.ts +17 -0
- package/src/worker-boundary.ts +395 -0
- package/src/worker-workspaces.ts +390 -0
- package/src/workspaces.ts +4 -2
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { realpathSync, readFileSync, lstatSync, mkdtempSync, mkdirSync, rmSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join, dirname, isAbsolute } from 'node:path';
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { promisify } from 'node:util';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { HarnessRpc } from './harness-rpc.js';
|
|
8
|
+
import { digest, ServiceError } from './protocol.js';
|
|
9
|
+
import { VERSION } from './config.js';
|
|
10
|
+
export const HARNESS_TARGETS = {
|
|
11
|
+
'gemini-acp': { dialect: 'acp', args: ['--acp'], version: '0.64.0' },
|
|
12
|
+
'claude-acp': { dialect: 'acp', args: [], version: '0.23.1' },
|
|
13
|
+
'codex-app-server': { dialect: 'codex', args: ['app-server'], version: '0.163.0' },
|
|
14
|
+
};
|
|
15
|
+
/** Operator-only negotiation. No repository, credential, task or model request is admitted. */
|
|
16
|
+
export async function probeHarness(pin) {
|
|
17
|
+
if (!Object.hasOwn(HARNESS_TARGETS, pin.target) || !isAbsolute(pin.executable))
|
|
18
|
+
throw new ServiceError('invalid_harness', 'Select a pinned supported absolute executable');
|
|
19
|
+
const target = HARNESS_TARGETS[pin.target], executable = realpathSync(pin.executable), stat = lstatSync(executable);
|
|
20
|
+
if (!stat.isFile() ||
|
|
21
|
+
stat.size > 512 * 1024 * 1024 ||
|
|
22
|
+
createHash('sha256').update(readFileSync(executable)).digest('hex') !== pin.executable_digest ||
|
|
23
|
+
pin.version !== target.version)
|
|
24
|
+
throw new ServiceError('harness_pin_changed', 'Harness executable digest or admitted version changed');
|
|
25
|
+
const dir = mkdtempSync(join(tmpdir(), 'sal-harness-probe-')), home = join(dir, 'home');
|
|
26
|
+
mkdirSync(home, { mode: 0o700 });
|
|
27
|
+
for (const name of ['.codex', '.config', '.cache'])
|
|
28
|
+
mkdirSync(join(home, name), { mode: 0o700 });
|
|
29
|
+
const env = {
|
|
30
|
+
HOME: home,
|
|
31
|
+
CODEX_HOME: join(home, '.codex'),
|
|
32
|
+
XDG_CONFIG_HOME: join(home, '.config'),
|
|
33
|
+
XDG_CACHE_HOME: join(home, '.cache'),
|
|
34
|
+
TMPDIR: dir,
|
|
35
|
+
PATH: [dirname(process.execPath), dirname(executable), '/usr/bin', '/bin'].join(':'),
|
|
36
|
+
LANG: 'en_US.UTF-8',
|
|
37
|
+
NO_COLOR: '1',
|
|
38
|
+
GEMINI_CLI_NO_RELAUNCH: 'true',
|
|
39
|
+
};
|
|
40
|
+
let rpc;
|
|
41
|
+
try {
|
|
42
|
+
const version = pin.target === 'claude-acp'
|
|
43
|
+
? {
|
|
44
|
+
stdout: (() => {
|
|
45
|
+
const metadata = JSON.parse(readFileSync(join(dirname(executable), '../package.json'), 'utf8'));
|
|
46
|
+
if (metadata.name !== '@zed-industries/claude-agent-acp')
|
|
47
|
+
throw new Error('Unexpected Claude ACP package');
|
|
48
|
+
return metadata.version;
|
|
49
|
+
})(),
|
|
50
|
+
}
|
|
51
|
+
: await promisify(execFile)(executable, ['--version'], {
|
|
52
|
+
cwd: dir,
|
|
53
|
+
env,
|
|
54
|
+
timeout: 10000,
|
|
55
|
+
maxBuffer: 16384,
|
|
56
|
+
});
|
|
57
|
+
if (!new RegExp('(^|[^0-9.])' + pin.version.replaceAll('.', '\\.') + '([^0-9.]|$)').test(version.stdout.trim()))
|
|
58
|
+
throw new ServiceError('harness_version_changed', 'Harness reported a different version');
|
|
59
|
+
const launch = {
|
|
60
|
+
executable,
|
|
61
|
+
args: [...target.args],
|
|
62
|
+
cwd: dir,
|
|
63
|
+
env,
|
|
64
|
+
dialect: target.dialect,
|
|
65
|
+
};
|
|
66
|
+
rpc = new HarnessRpc(launch);
|
|
67
|
+
const advertised = target.dialect === 'acp'
|
|
68
|
+
? await rpc.request('initialize', {
|
|
69
|
+
protocolVersion: 1,
|
|
70
|
+
clientInfo: { name: 'salaam', version: VERSION },
|
|
71
|
+
clientCapabilities: {
|
|
72
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
73
|
+
terminal: false,
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
: await rpc.request('initialize', {
|
|
77
|
+
clientInfo: { name: 'salaam', version: VERSION },
|
|
78
|
+
capabilities: { experimentalApi: false, requestAttestation: false },
|
|
79
|
+
});
|
|
80
|
+
if (target.dialect === 'acp' && advertised.protocolVersion !== 1)
|
|
81
|
+
throw new ServiceError('harness_protocol_changed', 'Harness did not negotiate ACP protocol version 1');
|
|
82
|
+
if (target.dialect === 'codex')
|
|
83
|
+
rpc.notify('initialized', {});
|
|
84
|
+
const enforced = {
|
|
85
|
+
files: false,
|
|
86
|
+
commands: false,
|
|
87
|
+
model_requests: false,
|
|
88
|
+
output_cap: false,
|
|
89
|
+
resume_dispatch: false,
|
|
90
|
+
isolation: 'unproven',
|
|
91
|
+
retention: 'unknown',
|
|
92
|
+
};
|
|
93
|
+
const admittedPin = { ...pin, executable };
|
|
94
|
+
return {
|
|
95
|
+
schema_version: 1,
|
|
96
|
+
pin: admittedPin,
|
|
97
|
+
launch_digest: digest({
|
|
98
|
+
pin: admittedPin,
|
|
99
|
+
args: target.args,
|
|
100
|
+
environment: 'empty-home-no-credentials-v1',
|
|
101
|
+
}),
|
|
102
|
+
capability_digest: digest({ advertised, enforced }),
|
|
103
|
+
advertised,
|
|
104
|
+
enforced,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
finally {
|
|
108
|
+
await rpc?.close();
|
|
109
|
+
rmSync(dir, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
import { HarnessRpc } from './harness-rpc.js';
|
|
2
|
+
import { digest, ServiceError } from './protocol.js';
|
|
3
|
+
const identity = (v) => {
|
|
4
|
+
if (typeof v !== 'string' || !v.length || v.length > 256)
|
|
5
|
+
throw new ServiceError('harness_identity', 'Missing or invalid upstream identity');
|
|
6
|
+
return v;
|
|
7
|
+
};
|
|
8
|
+
/** One owned invocation per adapter. Resume inspects the existing identity; it never resubmits a prompt. */
|
|
9
|
+
export class HarnessProtocol {
|
|
10
|
+
hooks;
|
|
11
|
+
rpc;
|
|
12
|
+
checkpoint;
|
|
13
|
+
pendingTurns = [];
|
|
14
|
+
terminal;
|
|
15
|
+
closing = false;
|
|
16
|
+
failed = false;
|
|
17
|
+
constructor(launch, checkpoint, hooks) {
|
|
18
|
+
this.hooks = hooks;
|
|
19
|
+
if (checkpoint.schema_version !== 1 ||
|
|
20
|
+
checkpoint.dialect !== launch.dialect ||
|
|
21
|
+
!checkpoint.worker_id ||
|
|
22
|
+
!checkpoint.attempt_id)
|
|
23
|
+
throw new Error('Harness checkpoint ownership mismatch');
|
|
24
|
+
this.checkpoint = structuredClone(checkpoint);
|
|
25
|
+
if (['creating', 'dispatching', 'running'].includes(this.checkpoint.state)) {
|
|
26
|
+
const recovered = { ...this.checkpoint, state: 'unknown' };
|
|
27
|
+
const saved = this.hooks.save(structuredClone(recovered));
|
|
28
|
+
if (saved !== undefined)
|
|
29
|
+
throw new Error('Harness recovery must commit synchronously');
|
|
30
|
+
this.checkpoint = recovered;
|
|
31
|
+
}
|
|
32
|
+
this.rpc = new HarnessRpc(launch, {
|
|
33
|
+
request: (method, params) => this.permission(method, params),
|
|
34
|
+
notification: (method, params) => this.notification(method, params),
|
|
35
|
+
failed: (error) => {
|
|
36
|
+
if (!this.closing) {
|
|
37
|
+
try {
|
|
38
|
+
this.unknown();
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
this.failed = true;
|
|
42
|
+
this.terminal?.reject(error);
|
|
43
|
+
this.hooks.failed?.(error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
view() {
|
|
50
|
+
return structuredClone(this.checkpoint);
|
|
51
|
+
}
|
|
52
|
+
save(update) {
|
|
53
|
+
this.assertUsable();
|
|
54
|
+
const next = { ...this.checkpoint, ...update };
|
|
55
|
+
try {
|
|
56
|
+
const result = this.hooks.save(structuredClone(next));
|
|
57
|
+
if (result !== undefined)
|
|
58
|
+
throw new Error('Harness checkpoint save must commit synchronously');
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
this.failed = true;
|
|
62
|
+
void this.rpc.close();
|
|
63
|
+
try {
|
|
64
|
+
this.hooks.failed?.(error);
|
|
65
|
+
}
|
|
66
|
+
catch { }
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
this.checkpoint = next;
|
|
70
|
+
}
|
|
71
|
+
assertUsable() {
|
|
72
|
+
if (this.failed)
|
|
73
|
+
throw new ServiceError('harness_control_failed', 'Harness control is no longer usable');
|
|
74
|
+
}
|
|
75
|
+
unknown() {
|
|
76
|
+
if (this.failed)
|
|
77
|
+
return;
|
|
78
|
+
if (!['terminal', 'prepared', 'idle'].includes(this.checkpoint.state))
|
|
79
|
+
this.save({ state: 'unknown' });
|
|
80
|
+
}
|
|
81
|
+
async initialize(gatewayAuth = false) {
|
|
82
|
+
this.assertUsable();
|
|
83
|
+
const result = await this.rpc.request('initialize', this.checkpoint.dialect === 'acp'
|
|
84
|
+
? {
|
|
85
|
+
protocolVersion: 1,
|
|
86
|
+
clientCapabilities: {
|
|
87
|
+
fs: { readTextFile: false, writeTextFile: false },
|
|
88
|
+
terminal: false,
|
|
89
|
+
...(gatewayAuth ? { auth: { _meta: { gateway: true } } } : {}),
|
|
90
|
+
},
|
|
91
|
+
clientInfo: { name: 'salaam', version: '0.0.1' },
|
|
92
|
+
}
|
|
93
|
+
: {
|
|
94
|
+
clientInfo: { name: 'salaam', version: '0.0.1' },
|
|
95
|
+
capabilities: { experimentalApi: false },
|
|
96
|
+
});
|
|
97
|
+
if (this.checkpoint.dialect === 'acp') {
|
|
98
|
+
if (result.protocolVersion !== 1)
|
|
99
|
+
throw new ServiceError('harness_protocol_changed', 'ACP v1 negotiation required');
|
|
100
|
+
const caps = result.agentCapabilities;
|
|
101
|
+
this.save({ load_session: caps?.loadSession === true });
|
|
102
|
+
}
|
|
103
|
+
else
|
|
104
|
+
this.rpc.notify('initialized', {});
|
|
105
|
+
return result;
|
|
106
|
+
}
|
|
107
|
+
async create(params) {
|
|
108
|
+
this.assertUsable();
|
|
109
|
+
if (this.checkpoint.state !== 'prepared' || this.checkpoint.session_id)
|
|
110
|
+
throw new ServiceError('harness_replay', 'An owned session intent cannot be recreated');
|
|
111
|
+
this.save({ state: 'creating' });
|
|
112
|
+
try {
|
|
113
|
+
const result = await this.rpc.request(this.checkpoint.dialect === 'acp' ? 'session/new' : 'thread/start', params, 30000);
|
|
114
|
+
const id = this.checkpoint.dialect === 'acp'
|
|
115
|
+
? identity(result.sessionId)
|
|
116
|
+
: identity(result.thread?.id);
|
|
117
|
+
this.save({ session_id: id, state: 'idle' });
|
|
118
|
+
return this.view();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
this.unknown();
|
|
122
|
+
throw error;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async prompt(text) {
|
|
126
|
+
this.assertUsable();
|
|
127
|
+
if (this.checkpoint.state !== 'idle' ||
|
|
128
|
+
!this.checkpoint.session_id ||
|
|
129
|
+
this.checkpoint.prompt_digest)
|
|
130
|
+
throw new ServiceError('harness_replay', 'This invocation already owns a prompt or uncertain work');
|
|
131
|
+
if (!text.trim() || Buffer.byteLength(text) > 65536 || text.trimStart().startsWith('/'))
|
|
132
|
+
throw new Error('Invalid external instruction');
|
|
133
|
+
this.save({ state: 'dispatching', prompt_digest: digest(text) });
|
|
134
|
+
try {
|
|
135
|
+
if (this.checkpoint.dialect === 'acp') {
|
|
136
|
+
const result = await this.rpc.request('session/prompt', { sessionId: this.checkpoint.session_id, prompt: [{ type: 'text', text }] }, 180000);
|
|
137
|
+
if (!['end_turn', 'max_tokens', 'max_turn_requests', 'refusal', 'cancelled'].includes(String(result.stopReason)))
|
|
138
|
+
throw new Error('Unknown ACP terminal stop reason');
|
|
139
|
+
this.save({ state: 'terminal', result });
|
|
140
|
+
return result;
|
|
141
|
+
}
|
|
142
|
+
const done = new Promise((resolve, reject) => {
|
|
143
|
+
this.terminal = { resolve, reject };
|
|
144
|
+
});
|
|
145
|
+
// Avoid an unhandled rejection if transport failure happens before turn/start returns.
|
|
146
|
+
void done.catch(() => { });
|
|
147
|
+
const result = await this.rpc.request('turn/start', { threadId: this.checkpoint.session_id, input: [{ type: 'text', text }] }, 30000);
|
|
148
|
+
const turn = identity(result.turn?.id);
|
|
149
|
+
this.save({ state: 'running', turn_id: turn });
|
|
150
|
+
for (const notification of this.pendingTurns.splice(0))
|
|
151
|
+
this.codexTerminal(notification);
|
|
152
|
+
if (this.checkpoint.cancel_requested &&
|
|
153
|
+
this.checkpoint.cancel_state === 'intent' &&
|
|
154
|
+
this.view().state !== 'terminal')
|
|
155
|
+
await this.forwardCancel();
|
|
156
|
+
const timer = setTimeout(() => {
|
|
157
|
+
try {
|
|
158
|
+
this.unknown();
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
this.terminal?.reject(error);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
this.terminal?.reject(new ServiceError('harness_outcome_unknown', 'No owned terminal turn receipt'));
|
|
165
|
+
}, 180000);
|
|
166
|
+
try {
|
|
167
|
+
return await done;
|
|
168
|
+
}
|
|
169
|
+
finally {
|
|
170
|
+
clearTimeout(timer);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
this.unknown();
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async cancel() {
|
|
179
|
+
this.assertUsable();
|
|
180
|
+
if (!['dispatching', 'running'].includes(this.checkpoint.state) || !this.checkpoint.session_id)
|
|
181
|
+
throw new ServiceError('harness_not_running', 'No running owned prompt');
|
|
182
|
+
if (this.checkpoint.cancel_requested)
|
|
183
|
+
return this.view();
|
|
184
|
+
this.save({ cancel_requested: true, cancel_state: 'intent' });
|
|
185
|
+
if (this.checkpoint.dialect === 'codex' && !this.checkpoint.turn_id)
|
|
186
|
+
return this.view();
|
|
187
|
+
await this.forwardCancel();
|
|
188
|
+
return this.view();
|
|
189
|
+
}
|
|
190
|
+
async forwardCancel() {
|
|
191
|
+
this.save({ cancel_state: 'dispatching' });
|
|
192
|
+
if (this.checkpoint.dialect === 'acp')
|
|
193
|
+
this.rpc.notify('session/cancel', { sessionId: this.checkpoint.session_id });
|
|
194
|
+
else {
|
|
195
|
+
if (!this.checkpoint.turn_id)
|
|
196
|
+
throw new ServiceError('harness_outcome_unknown', 'Turn acceptance is not yet known');
|
|
197
|
+
await this.rpc.request('turn/interrupt', {
|
|
198
|
+
threadId: this.checkpoint.session_id,
|
|
199
|
+
turnId: this.checkpoint.turn_id,
|
|
200
|
+
});
|
|
201
|
+
this.save({ cancel_state: 'acknowledged' });
|
|
202
|
+
}
|
|
203
|
+
// Acknowledgement is not proof of a terminal turn or stopped tools.
|
|
204
|
+
}
|
|
205
|
+
async inspect(params = {}) {
|
|
206
|
+
this.assertUsable();
|
|
207
|
+
if (!this.checkpoint.session_id)
|
|
208
|
+
throw new ServiceError('harness_outcome_unknown', 'No accepted upstream session identity');
|
|
209
|
+
if (this.checkpoint.dialect === 'acp') {
|
|
210
|
+
if (this.checkpoint.state === 'unknown')
|
|
211
|
+
throw new ServiceError('harness_outcome_unknown', 'ACP cannot safely reconcile an uncertain prompt; inspect retained artifacts and resolve explicitly');
|
|
212
|
+
if (!this.checkpoint.load_session)
|
|
213
|
+
throw new ServiceError('harness_resume_unsupported', 'Peer did not admit session loading');
|
|
214
|
+
const result = await this.rpc.request('session/load', {
|
|
215
|
+
...params,
|
|
216
|
+
sessionId: this.checkpoint.session_id,
|
|
217
|
+
});
|
|
218
|
+
// ACP history loading cannot prove which uncertain prompt completed.
|
|
219
|
+
return result;
|
|
220
|
+
}
|
|
221
|
+
const result = await this.rpc.request('thread/read', {
|
|
222
|
+
threadId: this.checkpoint.session_id,
|
|
223
|
+
includeTurns: true,
|
|
224
|
+
});
|
|
225
|
+
const thread = result.thread;
|
|
226
|
+
if (identity(thread?.id) !== this.checkpoint.session_id)
|
|
227
|
+
throw new ServiceError('harness_identity', 'Returned thread belongs to another worker');
|
|
228
|
+
if (this.checkpoint.turn_id && Array.isArray(thread.turns)) {
|
|
229
|
+
const turn = thread.turns.find((t) => t.id === this.checkpoint.turn_id);
|
|
230
|
+
if (turn && ['completed', 'failed', 'interrupted'].includes(String(turn.status)))
|
|
231
|
+
this.save({ state: 'terminal', result: { turn } });
|
|
232
|
+
}
|
|
233
|
+
return result;
|
|
234
|
+
}
|
|
235
|
+
async permission(method, params) {
|
|
236
|
+
this.assertUsable();
|
|
237
|
+
const state = this.checkpoint;
|
|
238
|
+
if (!state.session_id ||
|
|
239
|
+
state.cancel_requested ||
|
|
240
|
+
!['dispatching', 'running'].includes(state.state))
|
|
241
|
+
throw new ServiceError('harness_permission_denied', 'No active owned invocation');
|
|
242
|
+
if (state.dialect === 'acp') {
|
|
243
|
+
if (method !== 'session/request_permission' || params.sessionId !== state.session_id)
|
|
244
|
+
throw new ServiceError('harness_permission_denied', 'Client method or session is unowned');
|
|
245
|
+
identity(params.toolCall?.toolCallId);
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
if (!['item/commandExecution/requestApproval', 'item/fileChange/requestApproval'].includes(method) ||
|
|
249
|
+
params.threadId !== state.session_id ||
|
|
250
|
+
!state.turn_id ||
|
|
251
|
+
params.turnId !== state.turn_id)
|
|
252
|
+
throw new ServiceError('harness_permission_denied', 'Approval thread or turn is unowned');
|
|
253
|
+
identity(params.itemId);
|
|
254
|
+
}
|
|
255
|
+
const decision = await this.hooks.permission({
|
|
256
|
+
worker_id: state.worker_id,
|
|
257
|
+
attempt_id: state.attempt_id,
|
|
258
|
+
session_id: state.session_id,
|
|
259
|
+
turn_id: state.turn_id,
|
|
260
|
+
method,
|
|
261
|
+
params,
|
|
262
|
+
});
|
|
263
|
+
this.assertUsable();
|
|
264
|
+
if (this.checkpoint.cancel_requested ||
|
|
265
|
+
!['dispatching', 'running'].includes(this.checkpoint.state))
|
|
266
|
+
throw new ServiceError('harness_permission_denied', 'Owned invocation stopped before the decision');
|
|
267
|
+
if (decision !== 'approve' && decision !== 'deny')
|
|
268
|
+
throw new ServiceError('harness_permission_denied', 'Invalid local permission decision');
|
|
269
|
+
if (state.dialect === 'acp') {
|
|
270
|
+
const options = Array.isArray(params.options) ? params.options : [];
|
|
271
|
+
const option = options.find((o) => o.kind === (decision === 'approve' ? 'allow_once' : 'reject_once'));
|
|
272
|
+
if (!option || typeof option.optionId !== 'string')
|
|
273
|
+
throw new ServiceError('harness_permission_denied', 'Peer lacks a once-only permission choice');
|
|
274
|
+
return { outcome: { outcome: 'selected', optionId: option.optionId } };
|
|
275
|
+
}
|
|
276
|
+
return { decision: decision === 'approve' ? 'accept' : 'decline' };
|
|
277
|
+
}
|
|
278
|
+
notification(method, params) {
|
|
279
|
+
const state = this.checkpoint;
|
|
280
|
+
if (state.dialect === 'acp' && method === 'session/update') {
|
|
281
|
+
if (params.sessionId !== state.session_id) {
|
|
282
|
+
if (state.state === 'creating')
|
|
283
|
+
return;
|
|
284
|
+
throw new ServiceError('harness_identity', 'Unowned ACP update');
|
|
285
|
+
}
|
|
286
|
+
if (state.state === 'dispatching')
|
|
287
|
+
this.save({ state: 'running' });
|
|
288
|
+
}
|
|
289
|
+
else if (state.dialect === 'codex' && method.startsWith('turn/')) {
|
|
290
|
+
if (params.threadId !== state.session_id)
|
|
291
|
+
throw new ServiceError('harness_identity', 'Unowned Codex thread notification');
|
|
292
|
+
if (method === 'turn/completed') {
|
|
293
|
+
if (!state.turn_id && state.state === 'dispatching') {
|
|
294
|
+
if (this.pendingTurns.length >= 16)
|
|
295
|
+
throw Error('Unaccepted turn event bound');
|
|
296
|
+
this.pendingTurns.push(params);
|
|
297
|
+
}
|
|
298
|
+
else
|
|
299
|
+
this.codexTerminal(params);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
this.hooks.event?.(method, params);
|
|
303
|
+
}
|
|
304
|
+
codexTerminal(params) {
|
|
305
|
+
const turn = params.turn;
|
|
306
|
+
if (params.threadId !== this.checkpoint.session_id ||
|
|
307
|
+
identity(turn?.id) !== this.checkpoint.turn_id)
|
|
308
|
+
throw new ServiceError('harness_identity', 'Terminal receipt belongs to another turn');
|
|
309
|
+
if (!['completed', 'failed', 'interrupted'].includes(String(turn.status)))
|
|
310
|
+
throw new Error('Nonterminal Codex completion');
|
|
311
|
+
const result = { turn };
|
|
312
|
+
if (this.checkpoint.state === 'terminal') {
|
|
313
|
+
if (digest(this.checkpoint.result) !== digest(result))
|
|
314
|
+
throw new Error('Conflicting terminal receipt');
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
this.save({ state: 'terminal', result });
|
|
318
|
+
this.terminal?.resolve(result);
|
|
319
|
+
}
|
|
320
|
+
async close() {
|
|
321
|
+
this.closing = true;
|
|
322
|
+
this.unknown();
|
|
323
|
+
await this.rpc.close();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, realpathSync, rmSync, } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { HarnessRpc } from './harness-rpc.js';
|
|
5
|
+
/** Real readiness, not an advertised upstream capability. Runs without credentials or model traffic. */
|
|
6
|
+
export async function proveHarnessReadiness(boundary, root) {
|
|
7
|
+
const dir = mkdtempSync(join(realpathSync(root), 'harness-proof-'));
|
|
8
|
+
for (const p of ['work', 'home', 'runtime'])
|
|
9
|
+
mkdirSync(join(dir, p), { mode: 0o700 });
|
|
10
|
+
writeFileSync(join(dir, 'host-secret'), 'not-mounted');
|
|
11
|
+
writeFileSync(join(dir, 'runtime/readonly'), 'fixed');
|
|
12
|
+
const run = randomUUID();
|
|
13
|
+
let rpc;
|
|
14
|
+
try {
|
|
15
|
+
const script = `(async()=>{const fs=require('node:fs'),net=require('node:net'),cp=require('node:child_process'),rl=require('node:readline');for await(const line of rl.createInterface({input:process.stdin})){const m=JSON.parse(line);const denied=p=>{try{fs.readFileSync(p);return false}catch{return true}};const writeDenied=p=>{try{fs.writeFileSync(p,'bad');return false}catch{return true}};const connect=(host,port)=>new Promise(r=>{const s=net.connect({host,port});s.on('connect',()=>{s.destroy();r(true)});s.on('error',()=>r(false));s.setTimeout(500,()=>{s.destroy();r(false)})});fs.writeFileSync('/workspace/owned','ok');const bash=cp.execFileSync('/bin/bash',['-c','printf bash-ok'],{encoding:'utf8'});const response=await fetch('http://127.0.0.1:47123/proof',{method:'POST',body:'proof'});const result={host:denied(${JSON.stringify(join(dir, 'host-secret'))}),socket:denied('/var/run/docker.sock'),runtime:writeDenied('/runtime/readonly'),root:writeDenied('/escape'),bash,interfaces:fs.readdirSync('/sys/class/net'),external:await connect('1.1.1.1',53),metadata:await connect('169.254.169.254',80),other_port:await connect('127.0.0.1',47124),gateway:await response.text(),status:fs.readFileSync('/proc/self/status','utf8')};cp.spawn('/usr/local/bin/node',['-e',"setTimeout(()=>require('node:fs').writeFileSync('/workspace/late','bad'),1500)"],{detached:true,stdio:'ignore'}).unref();process.stdout.write(JSON.stringify({jsonrpc:'2.0',id:m.id,result})+'\\n')}})().catch(()=>process.exit(2));`;
|
|
16
|
+
const receipt = await boundary.prepare(run, join(dir, 'work'), join(dir, 'runtime'), join(dir, 'home'), ['/usr/local/bin/node', '-e', script]);
|
|
17
|
+
await boundary.gateway(receipt, async (req) => {
|
|
18
|
+
if (req.path !== '/proof' || req.body.toString() !== 'proof')
|
|
19
|
+
throw Error('Unexpected readiness traffic');
|
|
20
|
+
return { status: 200, body: Buffer.from('host-relay') };
|
|
21
|
+
});
|
|
22
|
+
rpc = new HarnessRpc(await boundary.launchHarness(receipt, 'acp'));
|
|
23
|
+
const proof = await rpc.request('proof', {}, 10000);
|
|
24
|
+
if (!proof.host ||
|
|
25
|
+
!proof.socket ||
|
|
26
|
+
!proof.runtime ||
|
|
27
|
+
!proof.root ||
|
|
28
|
+
proof.bash !== 'bash-ok' ||
|
|
29
|
+
proof.external ||
|
|
30
|
+
proof.metadata ||
|
|
31
|
+
proof.other_port ||
|
|
32
|
+
JSON.stringify(proof.interfaces) !== '["lo"]' ||
|
|
33
|
+
proof.gateway !== 'host-relay' ||
|
|
34
|
+
!/NoNewPrivs:\s+1/.test(String(proof.status)) ||
|
|
35
|
+
!/Seccomp:\s+2/.test(String(proof.status)) ||
|
|
36
|
+
!/CapEff:\s+0000000000000000/.test(String(proof.status)))
|
|
37
|
+
throw Error('Whole-harness readiness proof failed');
|
|
38
|
+
await rpc.close();
|
|
39
|
+
await boundary.cleanupRun(run);
|
|
40
|
+
await new Promise((r) => setTimeout(r, 1700));
|
|
41
|
+
if (existsSync(join(dir, 'work/late')) ||
|
|
42
|
+
readFileSync(join(dir, 'work/owned'), 'utf8') !== 'ok')
|
|
43
|
+
throw Error('Harness descendant stop proof failed');
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await rpc?.close();
|
|
47
|
+
await boundary.cleanupRun(run);
|
|
48
|
+
rmSync(dir, { recursive: true, force: true });
|
|
49
|
+
}
|
|
50
|
+
}
|