@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.11
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/agentpack/index.js +3 -1
- package/out/agentpack/paths.js +25 -2
- package/out/analytics/dashboard/localFleetAdapter.js +82 -2
- 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 +29 -25
- package/out/analytics/sessionEvents.js +100 -24
- 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 +126 -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 +151 -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 +351 -0
- package/out/cli/commands/serve.js +1145 -130
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +17 -0
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +18 -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 +225 -0
- package/out/cli/deploy/layers/identity.js +88 -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 +171 -17
- 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 +260 -14
- 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 +111 -27
- package/out/engines/custom.js +15 -1
- package/out/evidence/sessionShip.js +643 -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 +25 -2
- 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 +326 -0
- package/out/pack/lease-schema.js +273 -0
- package/out/pack/lease.js +682 -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 +141 -0
- package/out/runner/protocol.js +128 -0
- package/out/runner/remoteEngine.js +166 -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 +572 -0
- package/out/sandbox/processDriver.js +318 -0
- package/out/sandbox/sandboxRunner.js +303 -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 +266 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
- package/out/scripts/analytics/setup-analytics.ps1 +45 -14
- package/out/scripts/analytics/setup-analytics.sh +106 -15
- 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 +460 -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-engine-wiring.test.js +32 -0
- package/out/test/data-plane.test.js +380 -0
- package/out/test/deploy-init.test.js +47 -1
- 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 +222 -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 +224 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/localFleetAdapter.test.js +91 -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 +406 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/sandbox-scope.test.js +449 -0
- package/out/test/serve-cron-acceptance.test.js +370 -0
- package/out/test/serve-gateway-turn.test.js +122 -0
- package/out/test/serve-hardening.test.js +227 -15
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +618 -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
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createDataPlaneAuditSink = createDataPlaneAuditSink;
|
|
7
|
+
exports.dataPlaneSidecarCommand = dataPlaneSidecarCommand;
|
|
8
|
+
/**
|
|
9
|
+
* `agenthippo data-plane` — run the connector as a SIDECAR (secure-obo-design §7.6).
|
|
10
|
+
*
|
|
11
|
+
* This process is the connector's own trust domain: it holds the backend BASE credential
|
|
12
|
+
* (AWS keys, Postgres pool login, …) and runs in a SEPARATE container from the engine. The
|
|
13
|
+
* agent container's `serve` registers turns with this sidecar's authenticated control API and
|
|
14
|
+
* never sees the base credential.
|
|
15
|
+
*/
|
|
16
|
+
const fs_1 = require("fs");
|
|
17
|
+
const pack_loader_1 = require("../pack-loader");
|
|
18
|
+
const data_plane_1 = require("../data-plane");
|
|
19
|
+
const serve_hardening_1 = require("../serve-hardening");
|
|
20
|
+
const sessionEvents_1 = require("../../analytics/sessionEvents");
|
|
21
|
+
const identity_1 = require("../../analytics/identity");
|
|
22
|
+
const base_1 = require("../../engines/base");
|
|
23
|
+
const headless_1 = require("../../headless");
|
|
24
|
+
const utils_1 = require("../utils");
|
|
25
|
+
/**
|
|
26
|
+
* Build the sidecar's audit sink.
|
|
27
|
+
*
|
|
28
|
+
* Until this existed the sidecar passed no `audit` to `startDataPlaneSidecar`, and since every
|
|
29
|
+
* emit site in the data plane is `opts.audit?.(…)` the entire trust-boundary record —
|
|
30
|
+
* `tool_authorize`, `tool_deny`, `sidecar_register_reject`, `sidecar_edge_register` — was
|
|
31
|
+
* discarded in sidecar and edge mode: the two shapes the blueprints actually deploy. The
|
|
32
|
+
* "which user touched which rows" claim had no writer exactly where it was being made.
|
|
33
|
+
*
|
|
34
|
+
* `ok: false` means the directory could not be prepared. The caller decides what that costs,
|
|
35
|
+
* using the existing AGENTHIPPO_EVIDENCE_REQUIRED contract rather than a new knob.
|
|
36
|
+
*/
|
|
37
|
+
async function createDataPlaneAuditSink(stateDirOpt,
|
|
38
|
+
/**
|
|
39
|
+
* The pack THIS sidecar loaded — stamped onto every row so evidence answers "via which agent",
|
|
40
|
+
* not only "for which user" (PLAN/agent-identity.md §1 failure 3).
|
|
41
|
+
*
|
|
42
|
+
* There is no digest here on purpose: the sidecar does not verify a signature (`pack-loader.ts`
|
|
43
|
+
* has no signature code), so claiming one would dress an unverified read as an attestation.
|
|
44
|
+
* What serve ASSERTS about the pack arrives separately, per turn, via the register payload —
|
|
45
|
+
* and the two being different is the §4.4 detection.
|
|
46
|
+
*/
|
|
47
|
+
agent) {
|
|
48
|
+
const { dir: stateDir, ephemeral } = (0, serve_hardening_1.resolveStateDir)(stateDirOpt);
|
|
49
|
+
const auditDir = (0, serve_hardening_1.resolveAnalyticsSessionsDir)(stateDir, ephemeral);
|
|
50
|
+
let ok = true;
|
|
51
|
+
let error;
|
|
52
|
+
// Preflight, rather than discovering it on the first denied tool call.
|
|
53
|
+
try {
|
|
54
|
+
await fs_1.promises.mkdir(auditDir, { recursive: true });
|
|
55
|
+
await fs_1.promises.access(auditDir);
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
ok = false;
|
|
59
|
+
error = err instanceof Error ? err.message : String(err);
|
|
60
|
+
}
|
|
61
|
+
const writer = new sessionEvents_1.SessionEventWriter(auditDir);
|
|
62
|
+
const audit = (sessionKey, event, fields) => {
|
|
63
|
+
// Pseudonymise identity before it lands, matching the tool-event lane
|
|
64
|
+
// (ObservableStreamEmitter → hashIdentity). These records ship wholesale to the evidence
|
|
65
|
+
// server, so a raw address here leaves the machine; hashIdentity honours the operator's
|
|
66
|
+
// AGENTHIPPO_EVIDENCE_RAW_IDS / _ID_SALT choices so this lane makes the same trade as
|
|
67
|
+
// every other producer rather than a private one.
|
|
68
|
+
const rawUser = typeof fields.userId === 'string' ? fields.userId : undefined;
|
|
69
|
+
const anonUser = (0, identity_1.hashIdentity)(rawUser);
|
|
70
|
+
const safeFields = anonUser ? { ...fields, userId: anonUser } : fields;
|
|
71
|
+
// In EDGE mode the sidecar registers with `sessionKey: userId` (sidecar.ts:97), so the
|
|
72
|
+
// session key IS the identity and would otherwise become the evidence FILENAME —
|
|
73
|
+
// `alice@corp.com.jsonl`. Hash it the same way when, and only when, it is exactly the
|
|
74
|
+
// identity; a genuine session key is left alone so it still joins across lanes.
|
|
75
|
+
const safeSession = anonUser && sessionKey === rawUser ? anonUser : sessionKey;
|
|
76
|
+
void writer.append(safeSession, (0, base_1.generateRequestId)(), event, agent ? { ...safeFields, agent: { ...agent, digest: null } } : safeFields);
|
|
77
|
+
};
|
|
78
|
+
return { audit, auditDir, ephemeral, ok, error };
|
|
79
|
+
}
|
|
80
|
+
async function dataPlaneSidecarCommand(options) {
|
|
81
|
+
const registerToken = process.env.AGENTHIPPO_DATA_PLANE_TOKEN?.trim();
|
|
82
|
+
if (!registerToken) {
|
|
83
|
+
console.error('Error: AGENTHIPPO_DATA_PLANE_TOKEN is required (shared secret the trusted EDGE — or, legacy, the agent\'s serve — presents to register turns).');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
let pack;
|
|
87
|
+
try {
|
|
88
|
+
pack = await (0, pack_loader_1.loadAgentPack)(options.agent, options.workspace);
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const cfg = (0, data_plane_1.resolveDataPlaneConfig)(pack.manifest.spec, pack.packPath);
|
|
96
|
+
if (!cfg) {
|
|
97
|
+
console.error('Error: pack declares no spec.dataPlane — nothing for the sidecar to run.');
|
|
98
|
+
process.exit(1);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const logger = options.verbose ? new headless_1.ConsoleLogger('[Sidecar]') : new headless_1.NoOpLogger();
|
|
102
|
+
const { audit: auditEvent, auditDir, ephemeral, ok: auditOk, error: auditError } = await createDataPlaneAuditSink(options.stateDir, {
|
|
103
|
+
pack: pack.manifest.metadata.name,
|
|
104
|
+
version: pack.manifest.metadata.version ?? '0.0.0',
|
|
105
|
+
});
|
|
106
|
+
if (!auditOk) {
|
|
107
|
+
const detail = `${auditDir}: ${auditError}`;
|
|
108
|
+
// The existing AGENTHIPPO_EVIDENCE_REQUIRED contract decides what an unwritable sink
|
|
109
|
+
// costs — no new knob, and it already means "a hole in the record must fail the turn".
|
|
110
|
+
if ((0, sessionEvents_1.evidenceRequired)()) {
|
|
111
|
+
console.error(`${utils_1.COLORS.red}Error: data-plane audit directory is not writable${utils_1.COLORS.reset} — ${detail}`);
|
|
112
|
+
console.error(`AGENTHIPPO_EVIDENCE_REQUIRED is set, so refusing to run an unauditable data plane. Unset it to downgrade this to a warning.`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} data-plane audit directory is not writable — ${detail}`);
|
|
117
|
+
console.warn(' Tool authorizations and denials will NOT be recorded. Set --state-dir or AGENTHIPPO_HOME to a writable path.');
|
|
118
|
+
}
|
|
119
|
+
let sidecar;
|
|
120
|
+
try {
|
|
121
|
+
sidecar = await (0, data_plane_1.startDataPlaneSidecar)({
|
|
122
|
+
config: cfg,
|
|
123
|
+
registerToken,
|
|
124
|
+
gatewayPort: options.gatewayPort,
|
|
125
|
+
controlPort: options.controlPort,
|
|
126
|
+
bindHost: options.host ?? '0.0.0.0',
|
|
127
|
+
logger,
|
|
128
|
+
audit: auditEvent,
|
|
129
|
+
agent: {
|
|
130
|
+
pack: pack.manifest.metadata.name,
|
|
131
|
+
version: pack.manifest.metadata.version ?? '0.0.0',
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
console.error(`${utils_1.COLORS.red}Error: failed to start the data-plane sidecar:${utils_1.COLORS.reset} ${err instanceof Error ? err.message : err}`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
console.log(`OK: Data-plane sidecar (provider=${cfg.provider ?? 'passthrough'}) — connector: ${cfg.connectorPath}`);
|
|
141
|
+
console.log(` gateway (engine dials, nonce-auth): ${sidecar.gatewayUrl}`);
|
|
142
|
+
console.log(` control (edge/serve registers, bearer): :${sidecar.controlPort}`);
|
|
143
|
+
console.log(` audit (tool_authorize / tool_deny / register events): ${auditDir}${ephemeral ? ' (EPHEMERAL — lost on restart; set AGENTHIPPO_HOME or --state-dir)' : ''}`);
|
|
144
|
+
console.log(' Base credentials live here only — never in the agent container.');
|
|
145
|
+
console.log(' Edge mode (P1): give the register token to the IdP edge only; serve runs with AGENTHIPPO_DATA_PLANE_EDGE=1 and no token.');
|
|
146
|
+
const shutdown = () => { void sidecar.close().finally(() => process.exit(0)); };
|
|
147
|
+
process.on('SIGINT', shutdown);
|
|
148
|
+
process.on('SIGTERM', shutdown);
|
|
149
|
+
// Keep the process alive.
|
|
150
|
+
await new Promise(() => { });
|
|
151
|
+
}
|
|
@@ -215,6 +215,7 @@ async function deployCommand(action, packArg, options) {
|
|
|
215
215
|
cookieDomain: options.cookieDomain,
|
|
216
216
|
colocate: options.colocate,
|
|
217
217
|
modelRouting,
|
|
218
|
+
edgeTurnConnector: options.edgeTurnConnector?.trim() || undefined,
|
|
218
219
|
};
|
|
219
220
|
for (const target of targetsRequested) {
|
|
220
221
|
const routingSuffix = modelRouting === 'gateway' ? '-gateway' : '';
|
|
@@ -115,6 +115,10 @@ async function gatewaySendCommand(options) {
|
|
|
115
115
|
await sendViaEgressBroker(egressUrl, nonce, options);
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
|
+
if (egressUrl) {
|
|
119
|
+
console.error('Error: egress broker is active but AGENTHIPPO_TURN_NONCE is missing — refusing direct gateway send');
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
118
122
|
const storePath = resolveOutboundPath(options.outbound);
|
|
119
123
|
await ensureStoreFileExists(storePath);
|
|
120
124
|
const agentId = options.agent.trim();
|
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
|
+
}
|