@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.9
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/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +3 -2
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +23 -25
- package/out/analytics/sessionEvents.js +98 -23
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +48 -14
- package/out/auth/provider.js +8 -21
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +50 -5
- package/out/auth-broker/server.js +122 -26
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +67 -0
- package/out/cli/commands/deploy.js +1 -0
- package/out/cli/commands/gateway.js +4 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +332 -0
- package/out/cli/commands/serve.js +875 -52
- package/out/cli/commands/store.js +12 -4
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +17 -2
- package/out/cli/data-plane/registry.js +9 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +161 -0
- package/out/cli/deploy/layers/identity.js +85 -4
- package/out/cli/deploy-init.js +89 -24
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +35 -0
- package/out/cli/index.js +155 -16
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +8 -1
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +35 -1
- package/out/cli/serve-hardening.js +80 -3
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +33 -15
- package/out/engines/codex.js +110 -26
- package/out/engines/custom.js +8 -0
- package/out/evidence/sessionShip.js +610 -0
- package/out/extension.js +11 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +17 -1
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/mcp.js +30 -0
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +309 -0
- package/out/pack/lease-schema.js +270 -0
- package/out/pack/lease.js +640 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +131 -0
- package/out/runner/protocol.js +108 -0
- package/out/runner/remoteEngine.js +130 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +570 -0
- package/out/sandbox/processDriver.js +315 -0
- package/out/sandbox/sandboxRunner.js +191 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +182 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +94 -132
- package/out/scripts/analytics/setup-analytics.ps1 +37 -13
- package/out/scripts/analytics/setup-analytics.sh +93 -13
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +11 -110
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +162 -23
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +59 -9
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +455 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/data-plane.test.js +67 -0
- package/out/test/deploy-init.test.js +40 -0
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +217 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +169 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/mcp.test.js +27 -1
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +395 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/serve-hardening.test.js +45 -6
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +560 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +46 -9
- package/package.json +1 -1
package/out/cli/commands/pack.js
CHANGED
|
@@ -44,6 +44,10 @@ const fs = __importStar(require("fs"));
|
|
|
44
44
|
const path = __importStar(require("path"));
|
|
45
45
|
const pack_loader_1 = require("../pack-loader");
|
|
46
46
|
const branding_1 = require("../../branding");
|
|
47
|
+
const warrant_1 = require("../../authority/warrant");
|
|
48
|
+
const signature_1 = require("../../pack/signature");
|
|
49
|
+
const store_1 = require("./store");
|
|
50
|
+
const pack_install_ref_1 = require("../pack-install-ref");
|
|
47
51
|
const BANNER_LINE = '-'.repeat(62);
|
|
48
52
|
async function packCommand(options) {
|
|
49
53
|
const workspace = options.workspace;
|
|
@@ -65,9 +69,20 @@ async function packCommand(options) {
|
|
|
65
69
|
}
|
|
66
70
|
await showPackInfo(options.name, workspace);
|
|
67
71
|
break;
|
|
72
|
+
case 'sign':
|
|
73
|
+
await signPackCommand(options);
|
|
74
|
+
break;
|
|
75
|
+
case 'verify':
|
|
76
|
+
await verifyPackCommand(options);
|
|
77
|
+
break;
|
|
78
|
+
case 'digest':
|
|
79
|
+
await digestCommand(options);
|
|
80
|
+
break;
|
|
81
|
+
case 'keygen':
|
|
82
|
+
keygenCommand(options);
|
|
83
|
+
break;
|
|
68
84
|
case 'install':
|
|
69
|
-
|
|
70
|
-
console.log(` For now, manually copy your pack to ~/${branding_1.AGENTIDE_DIR_NAME}/agents/<name>/`);
|
|
85
|
+
await installPackCommand(options);
|
|
71
86
|
break;
|
|
72
87
|
case 'export':
|
|
73
88
|
console.log('Pack export is not yet implemented.');
|
|
@@ -79,11 +94,44 @@ async function packCommand(options) {
|
|
|
79
94
|
console.log(' list List installed packs');
|
|
80
95
|
console.log(' validate Validate a pack');
|
|
81
96
|
console.log(' info Show pack details');
|
|
82
|
-
console.log('
|
|
97
|
+
console.log(' sign Sign a pack so a fleet can verify it before running');
|
|
98
|
+
console.log(' verify Verify a pack against trusted public keys');
|
|
99
|
+
console.log(' digest Print the pack content digest (what a signature covers)');
|
|
100
|
+
console.log(' keygen Generate an ES256 signing keypair');
|
|
101
|
+
console.log(' install Install a pack from the store to a folder');
|
|
83
102
|
console.log(' export Export a pack (coming soon)');
|
|
84
103
|
process.exit(1);
|
|
85
104
|
}
|
|
86
105
|
}
|
|
106
|
+
async function installPackCommand(options) {
|
|
107
|
+
if (!options.name?.trim()) {
|
|
108
|
+
console.error('Error: pack slug required.');
|
|
109
|
+
console.log('Usage: agenthippo pack install <slug>[@version] --to <dir>');
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log('Examples:');
|
|
112
|
+
console.log(' agenthippo pack install joker1 --to ./packs/joker1');
|
|
113
|
+
console.log(' agenthippo pack install code-reviewer@1.0.0 --to ./packs/code-reviewer');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const dest = options.to?.trim();
|
|
117
|
+
if (!dest) {
|
|
118
|
+
console.error('Error: --to <dir> is required.');
|
|
119
|
+
console.log('Usage: agenthippo pack install <slug>[@version] --to <dir>');
|
|
120
|
+
console.log('');
|
|
121
|
+
console.log('Same download as `agenthippo store install pack/<slug>`, extracted flat for serve/deploy.');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
const { slug, version } = (0, pack_install_ref_1.parsePackInstallRef)(options.name);
|
|
125
|
+
await (0, store_1.installFromStore)({
|
|
126
|
+
action: 'install',
|
|
127
|
+
query: `pack/${slug}${version ? `@${version}` : ''}`,
|
|
128
|
+
workspace: options.workspace,
|
|
129
|
+
version,
|
|
130
|
+
force: options.force,
|
|
131
|
+
installDir: path.resolve(dest),
|
|
132
|
+
verbose: options.verbose,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
87
135
|
async function listPacks(workspace) {
|
|
88
136
|
const packs = (0, pack_loader_1.listAvailablePacks)(workspace);
|
|
89
137
|
if (packs.length === 0) {
|
|
@@ -218,3 +266,134 @@ async function showPackInfo(nameOrPath, workspace) {
|
|
|
218
266
|
process.exit(1);
|
|
219
267
|
}
|
|
220
268
|
}
|
|
269
|
+
// -----------------------------------------------------------------------------
|
|
270
|
+
// Signing
|
|
271
|
+
//
|
|
272
|
+
// Publishing has to be a one-liner before `--require-signed-pack` can be turned
|
|
273
|
+
// on in anger: a requirement nobody can satisfy just gets switched off again.
|
|
274
|
+
// -----------------------------------------------------------------------------
|
|
275
|
+
/** Resolve a pack path and its declared identity — a signature binds both, not just bytes. */
|
|
276
|
+
async function loadForSigning(nameOrPath, workspace) {
|
|
277
|
+
if (!nameOrPath) {
|
|
278
|
+
console.error('Error: pack name or path required');
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
const packPath = (0, pack_loader_1.resolvePackPath)(nameOrPath, workspace);
|
|
282
|
+
if (!packPath) {
|
|
283
|
+
console.error(`Error: pack not found: ${nameOrPath}`);
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
const loaded = await (0, pack_loader_1.loadAgentPack)(packPath, workspace);
|
|
287
|
+
return {
|
|
288
|
+
packPath: loaded.packPath,
|
|
289
|
+
name: loaded.manifest.metadata.name,
|
|
290
|
+
version: loaded.manifest.metadata.version ?? '0.0.0',
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function keygenCommand(options) {
|
|
294
|
+
const kid = options.kid || `sign-${new Date().toISOString().slice(0, 10)}`;
|
|
295
|
+
const key = (0, warrant_1.generateWarrantKeypair)(kid);
|
|
296
|
+
const out = options.output;
|
|
297
|
+
if (out) {
|
|
298
|
+
fs.writeFileSync(out, key.privateKeyPem, { mode: 0o600 });
|
|
299
|
+
console.log(`Private key written to ${out} (mode 0600) — keep it off shared disks.`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
console.log(key.privateKeyPem);
|
|
303
|
+
}
|
|
304
|
+
console.log('');
|
|
305
|
+
console.log(`kid: ${kid}`);
|
|
306
|
+
console.log('Public half — publish this to the control plane so instances can verify:');
|
|
307
|
+
console.log(key.publicKeyPem.trim());
|
|
308
|
+
console.log('');
|
|
309
|
+
console.log(` curl -X POST "$CONTROL/api/keys" -H 'content-type: application/json' \\`);
|
|
310
|
+
console.log(` -d '{"kid":"${kid}","publicPem":"…"}'`);
|
|
311
|
+
}
|
|
312
|
+
async function signPackCommand(options) {
|
|
313
|
+
const { packPath, name, version } = await loadForSigning(options.name, options.workspace);
|
|
314
|
+
if (!options.key) {
|
|
315
|
+
console.error('Error: --key <private-key.pem> required. Generate one with: agenthippo pack keygen -o key.pem');
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
let privateKeyPem;
|
|
319
|
+
try {
|
|
320
|
+
privateKeyPem = fs.readFileSync(options.key, 'utf8');
|
|
321
|
+
}
|
|
322
|
+
catch (err) {
|
|
323
|
+
console.error(`Error: cannot read key ${options.key}: ${err instanceof Error ? err.message : err}`);
|
|
324
|
+
process.exit(1);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const kid = options.kid;
|
|
328
|
+
if (!kid) {
|
|
329
|
+
console.error('Error: --kid <id> required — it is how a verifier finds the public half in JWKS.');
|
|
330
|
+
process.exit(1);
|
|
331
|
+
}
|
|
332
|
+
const { digest } = (0, signature_1.signPack)({ packDir: packPath, pack: name, packVersion: version, privateKeyPem, kid: kid });
|
|
333
|
+
console.log(`Signed ${name}@${version}`);
|
|
334
|
+
console.log(` digest ${digest}`);
|
|
335
|
+
console.log(` key ${kid}`);
|
|
336
|
+
console.log(` written ${path.join(packPath, signature_1.PACK_SIGNATURE_FILE)}`);
|
|
337
|
+
console.log('');
|
|
338
|
+
console.log('Commit pack.sig with the pack: it travels WITH the artifact, and a fleet that');
|
|
339
|
+
console.log('requires signatures refuses anything whose contents no longer match it.');
|
|
340
|
+
}
|
|
341
|
+
async function verifyPackCommand(options) {
|
|
342
|
+
const { packPath, name, version } = await loadForSigning(options.name, options.workspace);
|
|
343
|
+
const token = (0, signature_1.readPackSignature)(packPath);
|
|
344
|
+
if (!token) {
|
|
345
|
+
console.error(`UNSIGNED: no ${signature_1.PACK_SIGNATURE_FILE} at ${packPath}`);
|
|
346
|
+
console.error((0, signature_1.describePackRefusal)({ kind: 'unsigned', detail: `no ${signature_1.PACK_SIGNATURE_FILE}`, pack: name, packVersion: version, packDir: packPath }));
|
|
347
|
+
process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
let keys = {};
|
|
350
|
+
if (options.keys) {
|
|
351
|
+
try {
|
|
352
|
+
keys = JSON.parse(fs.readFileSync(options.keys, 'utf8'));
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
console.error(`Error: cannot read keys ${options.keys}: ${err instanceof Error ? err.message : err}`);
|
|
356
|
+
process.exit(1);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
const trust = await (0, signature_1.loadPackTrustRoot)({
|
|
361
|
+
env: options.jwks ? { ...process.env, [signature_1.PACK_JWKS_URL_ENV]: options.jwks } : process.env,
|
|
362
|
+
leaseUrl: process.env.AGENTHIPPO_LEASE_URL,
|
|
363
|
+
});
|
|
364
|
+
for (const w of trust.warnings) {
|
|
365
|
+
console.error(`Warning: ${w}`);
|
|
366
|
+
}
|
|
367
|
+
if (trust.status === 'unavailable') {
|
|
368
|
+
// Distinct from "no keys configured", and with a different fix. Collapsing the two into
|
|
369
|
+
// an empty keyring is finding A4 — a 503 read exactly like an unmanaged install.
|
|
370
|
+
console.error(`REFUSED: a trust root is configured but could not be loaded — ${trust.reason}`);
|
|
371
|
+
console.error((0, signature_1.describePackRefusal)({
|
|
372
|
+
kind: 'trust-root-unavailable', detail: trust.reason ?? 'unavailable', pack: name, packVersion: version,
|
|
373
|
+
packDir: packPath, trustRootHint: trust.url ? `Trust root: ${trust.url}` : undefined,
|
|
374
|
+
}));
|
|
375
|
+
process.exit(1);
|
|
376
|
+
}
|
|
377
|
+
keys = trust.keys;
|
|
378
|
+
}
|
|
379
|
+
if (Object.keys(keys).length === 0) {
|
|
380
|
+
// "No keys" is a refusal, never a pass: verifying against an empty keyring would report
|
|
381
|
+
// success for any signature at all.
|
|
382
|
+
console.error('REFUSED: no trusted public keys. Pass --keys <file>, --jwks <url>, or set AGENTHIPPO_PACK_SIGNING_KEYS.');
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
const res = (0, signature_1.verifyPack)(token, keys, { packDir: packPath, pack: name, packVersion: version });
|
|
386
|
+
if (!res.ok) {
|
|
387
|
+
console.error(`INVALID: ${res.reason}`);
|
|
388
|
+
console.error((0, signature_1.describePackRefusal)({ kind: res.kind, detail: res.reason, pack: name, packVersion: version, packDir: packPath }));
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
console.log(`OK: ${name}@${version} verified`);
|
|
392
|
+
console.log(` digest ${res.claims.digest}`);
|
|
393
|
+
console.log(` NOTE: .claude/, .codex/ and .agent-hippo/ ARE covered by this digest — they carry hooks`);
|
|
394
|
+
console.log(' and other executable content, and were previously outside the signature.');
|
|
395
|
+
}
|
|
396
|
+
async function digestCommand(options) {
|
|
397
|
+
const { packPath, name, version } = await loadForSigning(options.name, options.workspace);
|
|
398
|
+
console.log(`${(0, signature_1.computePackDigest)(packPath)} ${name}@${version}`);
|
|
399
|
+
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.resolveSandboxWorkspace = resolveSandboxWorkspace;
|
|
40
|
+
exports.isForeignWorkspace = isForeignWorkspace;
|
|
41
|
+
exports.sandboxCommand = sandboxCommand;
|
|
42
|
+
/**
|
|
43
|
+
* `agenthippo sandbox` — the in-sandbox runner entrypoint (PLAN/serve-v4-clean.md §3–§4).
|
|
44
|
+
*
|
|
45
|
+
* This is the process that runs INSIDE a sandbox. It owns the real engine and speaks the frame
|
|
46
|
+
* protocol out over either a unix socket (the `container` driver) or stdio (the `process`
|
|
47
|
+
* driver). It holds no credential: the model is reached through `baseUrl` + the per-sandbox
|
|
48
|
+
* `modelToken` that arrives on each run frame, and data through the gateway URL with a
|
|
49
|
+
* per-turn nonce. Nothing here can mint authority.
|
|
50
|
+
*/
|
|
51
|
+
const fs = __importStar(require("fs"));
|
|
52
|
+
const net = __importStar(require("net"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const headless_1 = require("../../headless");
|
|
55
|
+
const utils_1 = require("../utils");
|
|
56
|
+
const pack_loader_1 = require("../pack-loader");
|
|
57
|
+
const pack_runtime_1 = require("../pack-runtime");
|
|
58
|
+
const deps_1 = require("../deps");
|
|
59
|
+
const serve_hardening_1 = require("../serve-hardening");
|
|
60
|
+
const runtime_1 = require("../runtime");
|
|
61
|
+
const host_1 = require("../../runner/host");
|
|
62
|
+
/**
|
|
63
|
+
* The sandbox's OWN workspace is authoritative — never the one on the wire.
|
|
64
|
+
*
|
|
65
|
+
* serve sends its HOST workspace path on every run frame. Across a container boundary that path
|
|
66
|
+
* is meaningless, and honouring it was the bug behind "codex discards the write": the sandbox
|
|
67
|
+
* mkdir'd serve's host path INSIDE the sandbox and ran the engine there, so every file the engine
|
|
68
|
+
* wrote landed in a container-private directory — present via ls/cat inside the sandbox during the
|
|
69
|
+
* turn, absent on the host, gone when the sandbox died. The engine also ran in a directory holding
|
|
70
|
+
* none of the synced pack content. Proven with a probe: a sandbox handed a workspaceRoot that
|
|
71
|
+
* exists on no host created it and worked there.
|
|
72
|
+
*
|
|
73
|
+
* At tier 0 the two are the same path (serve passes `--workspace`), so nothing changes there.
|
|
74
|
+
*/
|
|
75
|
+
function resolveSandboxWorkspace(_wireRoot, sandboxWorkspace) {
|
|
76
|
+
return sandboxWorkspace;
|
|
77
|
+
}
|
|
78
|
+
/** True when the wire asked for a workspace this sandbox will not use — worth saying out loud once. */
|
|
79
|
+
function isForeignWorkspace(wireRoot, sandboxWorkspace) {
|
|
80
|
+
if (!wireRoot) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
return path.resolve(wireRoot) !== path.resolve(sandboxWorkspace);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Adapts the headless runner to `AgentEngine` so `startRunnerHost` can drive it. The model
|
|
87
|
+
* credential is *per-run*: the sandbox rebuilds its runner if the token changes (rotation), and
|
|
88
|
+
* never persists a provider key of its own.
|
|
89
|
+
*/
|
|
90
|
+
class SandboxEngine {
|
|
91
|
+
defaults;
|
|
92
|
+
id = 'sandbox';
|
|
93
|
+
displayName = 'Sandbox runner';
|
|
94
|
+
runner;
|
|
95
|
+
builtWith;
|
|
96
|
+
/** Log the wire/sandbox workspace mismatch once, not once per turn. */
|
|
97
|
+
warnedWorkspace = false;
|
|
98
|
+
/** Last native engine session id — rides the terminal frame for resume after sandbox death. */
|
|
99
|
+
nativeSessionId;
|
|
100
|
+
constructor(defaults) {
|
|
101
|
+
this.defaults = defaults;
|
|
102
|
+
}
|
|
103
|
+
ensureRunner(opts) {
|
|
104
|
+
const key = `${opts.baseUrl ?? ''}|${opts.modelToken ?? ''}`;
|
|
105
|
+
if (this.runner && this.builtWith === key) {
|
|
106
|
+
return this.runner;
|
|
107
|
+
}
|
|
108
|
+
this.runner?.dispose();
|
|
109
|
+
// The same factory serve builds its own runner with — engine executables, bundled base
|
|
110
|
+
// prompts and the child env patch all resolve identically on both sides of the boundary.
|
|
111
|
+
this.runner = (0, pack_runtime_1.createPackRunner)({
|
|
112
|
+
storagePath: this.defaults.storagePath,
|
|
113
|
+
analyticsSessionsDir: this.defaults.analyticsSessionsDir,
|
|
114
|
+
packId: this.defaults.packId,
|
|
115
|
+
packVersion: this.defaults.packVersion,
|
|
116
|
+
provenance: () => (0, runtime_1.readProvenanceEnvFromProcess)(),
|
|
117
|
+
workspaceRoot: this.defaults.workspace,
|
|
118
|
+
credentials: {
|
|
119
|
+
baseUrl: opts.baseUrl,
|
|
120
|
+
litellmApiKey: opts.modelToken,
|
|
121
|
+
useLiteLLM: Boolean(opts.baseUrl),
|
|
122
|
+
},
|
|
123
|
+
claudeCheapModel: this.defaults.claudeCheapModel,
|
|
124
|
+
omitGatewayToken: true,
|
|
125
|
+
});
|
|
126
|
+
this.builtWith = key;
|
|
127
|
+
return this.runner;
|
|
128
|
+
}
|
|
129
|
+
async stream(message, options, emitter, _runtime, signal) {
|
|
130
|
+
const wire = options;
|
|
131
|
+
const wireRoot = wire.workspaceRoot;
|
|
132
|
+
const workspaceRoot = resolveSandboxWorkspace(wireRoot, this.defaults.workspace);
|
|
133
|
+
if (isForeignWorkspace(wireRoot, workspaceRoot) && !this.warnedWorkspace) {
|
|
134
|
+
this.warnedWorkspace = true;
|
|
135
|
+
console.warn(`[sandbox] ignoring workspaceRoot ${wireRoot} from the wire — this sandbox works in ${workspaceRoot}`);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
fs.mkdirSync(workspaceRoot, { recursive: true });
|
|
139
|
+
// A sandbox hosts exactly one session, so its working directory IS that session's
|
|
140
|
+
// workspace. Without the chdir the engine inherits the sandbox process's cwd and a tool
|
|
141
|
+
// that writes a RELATIVE path (codex runs `sh -lc 'printf ... > proof.txt'`) lands the
|
|
142
|
+
// file outside the workspace — it exits 0, the agent reports success, work is lost.
|
|
143
|
+
process.chdir(workspaceRoot);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
throw new Error(`sandbox cannot use workspace ${workspaceRoot}: ${err instanceof Error ? err.message : String(err)}`);
|
|
147
|
+
}
|
|
148
|
+
const runner = this.ensureRunner(wire);
|
|
149
|
+
// Permissions MUST cross: they decide the ENGINE's own sandbox mode (codex maps
|
|
150
|
+
// workspace-write → sandboxMode=workspace-write, read-only → read-only). Dropping them
|
|
151
|
+
// silently downgraded every sandbox turn to the default, so codex's shell exited 0 while its
|
|
152
|
+
// sandbox discarded the write — reads and commands worked, files never appeared.
|
|
153
|
+
// Observed live on tier 2. Accept either serve's nested `permissions` or a bare
|
|
154
|
+
// `fileAccess` on the options.
|
|
155
|
+
const loose = wire;
|
|
156
|
+
// Last resort is the PACK's own declared fileAccess, not the engine default: a frame that
|
|
157
|
+
// somehow arrives without permissions must not hand the engine more access than the
|
|
158
|
+
// manifest asks for.
|
|
159
|
+
const perms = loose.permissions
|
|
160
|
+
?? (loose.fileAccess ? { fileAccess: loose.fileAccess } : { fileAccess: this.defaults.fileAccess });
|
|
161
|
+
const result = await runner.run({
|
|
162
|
+
engineId: wire.engineId ?? this.defaults.engineId,
|
|
163
|
+
modelId: wire.modelId ?? this.defaults.modelId,
|
|
164
|
+
message,
|
|
165
|
+
workspaceRoot,
|
|
166
|
+
additionalDirectories: wire.additionalDirectories,
|
|
167
|
+
sessionId: wire.sessionId,
|
|
168
|
+
permissions: perms,
|
|
169
|
+
// Parity with serve's in-process runner: attribution for evidence, the per-turn
|
|
170
|
+
// data-plane nonce (without which a sandbox cannot reach the data gateway at all), and
|
|
171
|
+
// the bound gateway session.
|
|
172
|
+
analyticsUserId: wire.analyticsUserId,
|
|
173
|
+
turnEnv: wire.turnEnv,
|
|
174
|
+
gatewaySessionKey: wire.gatewaySessionKey,
|
|
175
|
+
emitter,
|
|
176
|
+
signal,
|
|
177
|
+
});
|
|
178
|
+
// A failed run must reject so the host emits `failed`, not `result` — otherwise serve
|
|
179
|
+
// would record a successful turn for a turn that errored.
|
|
180
|
+
if (!result.success && result.error) {
|
|
181
|
+
throw new Error(result.error);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
onMaintenance() { }
|
|
185
|
+
dispose() { this.runner?.dispose(); }
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* A sandbox hosts ONE session for its whole life, so an engine-side async error must not take the
|
|
189
|
+
* process down — that would lose the session, not just the turn. Vendor SDKs reject outside the
|
|
190
|
+
* `engine.stream()` await chain (observed: the Claude SDK's `streamInput` rejecting after its
|
|
191
|
+
* child process dies), which Node treats as fatal by default. Log and stay up; the turn's own
|
|
192
|
+
* catch still emits `failed`, and serve can always destroy the sandbox if it wants it gone.
|
|
193
|
+
*
|
|
194
|
+
* `uncaughtException` is NOT swallowed: after one the process state is unreliable, so exit and
|
|
195
|
+
* let the driver create a fresh sandbox.
|
|
196
|
+
*/
|
|
197
|
+
function installSandboxResilience() {
|
|
198
|
+
process.on('unhandledRejection', (reason) => {
|
|
199
|
+
const msg = reason instanceof Error ? reason.message : String(reason);
|
|
200
|
+
console.error(`[sandbox] unhandled rejection (session kept alive): ${msg}`);
|
|
201
|
+
});
|
|
202
|
+
process.on('uncaughtException', (err) => {
|
|
203
|
+
console.error(`[sandbox] uncaught exception — exiting so the driver recreates this sandbox: ${err.message}`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function sandboxCommand(options) {
|
|
208
|
+
// Every other command does this in its action; a sandbox needs it too, or a dev-box sandbox has no
|
|
209
|
+
// engine SDKs at all. In a self-contained image AGENTHIPPO_NO_AUTO_INSTALL=1 makes it a no-op.
|
|
210
|
+
await (0, deps_1.ensureDeps)(false);
|
|
211
|
+
const stateDir = options.stateDir ?? '/state';
|
|
212
|
+
const workspaceForLoad = options.workspace ?? path.join(stateDir, 'workspace');
|
|
213
|
+
fs.mkdirSync(workspaceForLoad, { recursive: true });
|
|
214
|
+
// The real loader, exactly as serve uses it — and `--agent` is a name-OR-path for the same
|
|
215
|
+
// reason it is everywhere else. The previous code resolved it as a path and demanded
|
|
216
|
+
// agent.yaml there, so `serve --agent joker1 --sandbox process` handed the sandbox a bare name
|
|
217
|
+
// it turned into <cwd>/joker1 and every turn died with "No agent.yaml". The hand-rolled regex
|
|
218
|
+
// before that only matched block-style YAML, so a flow-style manifest (`metadata: { name: x }`)
|
|
219
|
+
// silently fell back to 'default' — which is how CODEX_HOME ended up at agents/default instead
|
|
220
|
+
// of agents/<pack>, breaking per-pack engine isolation inside sandboxes.
|
|
221
|
+
const pack = await (0, pack_loader_1.loadAgentPack)(options.agent, workspaceForLoad);
|
|
222
|
+
const manifest = {
|
|
223
|
+
name: pack.manifest.metadata.name,
|
|
224
|
+
version: pack.manifest.metadata.version,
|
|
225
|
+
engine: pack.manifest.spec?.engine ?? 'claude',
|
|
226
|
+
};
|
|
227
|
+
// A sandbox runs on a read-only root by design, so every writable path must live under the one
|
|
228
|
+
// mounted state directory. `--state-dir` is authoritative here and OVERRIDES the image's
|
|
229
|
+
// baked AGENTHIPPO_HOME (the runtime image sets /state, which is read-only unless mounted);
|
|
230
|
+
// otherwise the engine's cache lands on the read-only root and the first turn dies with
|
|
231
|
+
// EROFS — observed live on Linux.
|
|
232
|
+
if (options.stateDir) {
|
|
233
|
+
process.env.AGENTHIPPO_HOME = stateDir;
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
process.env.AGENTHIPPO_HOME = process.env.AGENTHIPPO_HOME ?? stateDir;
|
|
237
|
+
}
|
|
238
|
+
// HOME too, not just AGENTHIPPO_HOME: engine SDKs write to dot-directories under $HOME
|
|
239
|
+
// (the Cursor SDK uses ~/.cursor, codex/claude keep caches there), and the image's default
|
|
240
|
+
// HOME sits on the read-only root — so every such write fails with ENOENT/EROFS inside a
|
|
241
|
+
// sandbox. Observed live on tier 2 with the Cursor SDK.
|
|
242
|
+
// Declare that a sandbox surrounds this process. This says WHERE we are, not how confined we
|
|
243
|
+
// are: whether an engine may stand its own boundary down is answered by AGENTHIPPO_SANDBOX_TIER
|
|
244
|
+
// (see sandbox/types.ts), which only the driver sets and only tier 2 satisfies. Deriving
|
|
245
|
+
// "unconfined is safe" from this flag is what made `--sandbox process` weaker than no sandbox.
|
|
246
|
+
process.env.AGENTHIPPO_SANDBOX = '1';
|
|
247
|
+
const sandboxHome = path.join(stateDir, 'home');
|
|
248
|
+
fs.mkdirSync(sandboxHome, { recursive: true });
|
|
249
|
+
process.env.HOME = sandboxHome;
|
|
250
|
+
// Hydrate the workspace from the mounted pack — the SAME call serve makes at boot. A sandbox
|
|
251
|
+
// holds exactly one user's session, so it should look exactly like the single-user case:
|
|
252
|
+
// pack content (skills, rules, engine config) lives IN the workspace, and every downstream
|
|
253
|
+
// path (`getSkillsSignature(workspaceRoot)`, engine homes) resolves off that one root.
|
|
254
|
+
// Without this the pack stayed a read-only mount the engines never looked at, so no pack
|
|
255
|
+
// skill was ever visible inside a sandbox — observed live on tier 2.
|
|
256
|
+
const workspace = workspaceForLoad;
|
|
257
|
+
await (0, headless_1.syncPackToWorkspace)(workspace, pack.packPath, manifest.name, manifest.engine, console, (0, utils_1.findBundledBasePromptsDir)());
|
|
258
|
+
console.log(`[sandbox] pack ${manifest.name}@${manifest.version ?? '?'} synced into ${workspace} (engine ${manifest.engine})`);
|
|
259
|
+
// Only NOW: everything above is boot, and a boot failure must be fatal so the driver sees a
|
|
260
|
+
// dead sandbox and reports the real error. Swallowing it would leave a sandbox that answers every
|
|
261
|
+
// turn with "channel closed" and no cause.
|
|
262
|
+
installSandboxResilience();
|
|
263
|
+
const rawCheapModel = pack.manifest.spec?.cheapModel?.trim();
|
|
264
|
+
const engine = new SandboxEngine({
|
|
265
|
+
storagePath: stateDir,
|
|
266
|
+
// Land evidence where serve and Spotlight already scan, rather than the headless default.
|
|
267
|
+
analyticsSessionsDir: (0, serve_hardening_1.resolveAnalyticsSessionsDir)(stateDir, false),
|
|
268
|
+
packId: manifest.name,
|
|
269
|
+
packVersion: manifest.version,
|
|
270
|
+
engineId: manifest.engine,
|
|
271
|
+
// serve normally sends modelId on every frame; this is the fallback, and it comes from the
|
|
272
|
+
// pack the same way serve's does instead of a hardcoded alias.
|
|
273
|
+
modelId: (0, headless_1.resolveModelAlias)(pack.manifest.spec?.model?.trim() || 'ah-auto'),
|
|
274
|
+
claudeCheapModel: rawCheapModel ? (0, headless_1.resolveModelAlias)(rawCheapModel) : undefined,
|
|
275
|
+
fileAccess: (0, pack_loader_1.getFileAccess)(pack),
|
|
276
|
+
workspace,
|
|
277
|
+
});
|
|
278
|
+
const runtime = { logger: console };
|
|
279
|
+
if (options.stdio) {
|
|
280
|
+
const host = (0, host_1.startRunnerHost)({
|
|
281
|
+
engine, runtime,
|
|
282
|
+
input: process.stdin,
|
|
283
|
+
output: { write: (c) => process.stdout.write(c) },
|
|
284
|
+
nativeSessionId: () => engine.nativeSessionId,
|
|
285
|
+
});
|
|
286
|
+
await host.done;
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const socketPath = options.socket;
|
|
290
|
+
if (!socketPath) {
|
|
291
|
+
throw new Error('sandbox requires --socket <path> or --stdio');
|
|
292
|
+
}
|
|
293
|
+
// A stale socket from a previous sandbox would make bind fail; the sandbox owns this path.
|
|
294
|
+
try {
|
|
295
|
+
fs.unlinkSync(socketPath);
|
|
296
|
+
}
|
|
297
|
+
catch { /* not there — fine */ }
|
|
298
|
+
const server = net.createServer((conn) => {
|
|
299
|
+
(0, host_1.startRunnerHost)({
|
|
300
|
+
engine, runtime,
|
|
301
|
+
input: conn,
|
|
302
|
+
output: { write: (c) => conn.write(c) },
|
|
303
|
+
nativeSessionId: () => engine.nativeSessionId,
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
// Binding is fatal: a sandbox nobody can reach is useless, and exiting 0 would tell the driver
|
|
307
|
+
// the sandbox is healthy while every turn fails on a dead channel (observed live).
|
|
308
|
+
try {
|
|
309
|
+
await new Promise((resolve, reject) => {
|
|
310
|
+
server.once('error', reject);
|
|
311
|
+
server.listen(socketPath, () => resolve());
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
catch (err) {
|
|
315
|
+
console.error(`[sandbox] FATAL: cannot listen at ${socketPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
316
|
+
process.exit(1);
|
|
317
|
+
}
|
|
318
|
+
// 0o660: the sandbox owns the socket; serve connects from the host. serve manages the container
|
|
319
|
+
// runtime (already root-equivalent), so it can connect; no other local uid can. Only the
|
|
320
|
+
// sandbox's own mount namespace has this path at all — the mode is defence in depth.
|
|
321
|
+
try {
|
|
322
|
+
fs.chmodSync(socketPath, 0o660);
|
|
323
|
+
}
|
|
324
|
+
catch { /* best effort */ }
|
|
325
|
+
console.log(`[sandbox] ${options.sandboxId ?? 'sandbox'} listening on ${socketPath} (pack ${manifest.name}@${manifest.version ?? '?'})`);
|
|
326
|
+
await new Promise((resolve) => {
|
|
327
|
+
const shutdown = () => { server.close(() => resolve()); engine.dispose(); };
|
|
328
|
+
process.once('SIGTERM', shutdown);
|
|
329
|
+
process.once('SIGINT', shutdown);
|
|
330
|
+
server.once('close', () => resolve());
|
|
331
|
+
});
|
|
332
|
+
}
|