@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
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* The LEASE trust root — the keyring that decides which policy this instance obeys.
|
|
4
|
+
*
|
|
5
|
+
* Signed leases were unreachable end to end. serve verified lease envelopes with
|
|
6
|
+
* `signingKeys: () => packTrust.keys` — the PACK trust root — while the control plane publishes
|
|
7
|
+
* the lease public key under purpose `lease-signing` and refuses to register one kid under two
|
|
8
|
+
* purposes. So a correctly-configured plane and a correctly-configured serve could not name the
|
|
9
|
+
* same key, and `requireSignedLease` (sticky, with no plane-side undo) would latch an instance
|
|
10
|
+
* into refusing turns with no way back from the console.
|
|
11
|
+
*--------------------------------------------------------------------------------------------*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
require("mocha");
|
|
47
|
+
const assert = __importStar(require("assert"));
|
|
48
|
+
const warrant_1 = require("../authority/warrant");
|
|
49
|
+
const lease_1 = require("../pack/lease");
|
|
50
|
+
const signature_1 = require("../pack/signature");
|
|
51
|
+
const leaseKey = (0, warrant_1.generateWarrantKeypair)('lease-2026');
|
|
52
|
+
const packKey = (0, warrant_1.generateWarrantKeypair)('pack-2026');
|
|
53
|
+
const NOW = 1_800_000_000;
|
|
54
|
+
const lease = { pack: 'analyst', expiresAt: NOW + 3600, enabled: true };
|
|
55
|
+
const okResponse = (body) => ({
|
|
56
|
+
ok: true,
|
|
57
|
+
status: 200,
|
|
58
|
+
json: async () => body,
|
|
59
|
+
});
|
|
60
|
+
const fetchReturning = (body) => (async () => okResponse(body));
|
|
61
|
+
const bodyResponse = (text) => ({
|
|
62
|
+
ok: true,
|
|
63
|
+
status: 200,
|
|
64
|
+
text: async () => text,
|
|
65
|
+
json: async () => JSON.parse(text),
|
|
66
|
+
});
|
|
67
|
+
const collectLogs = () => {
|
|
68
|
+
const lines = [];
|
|
69
|
+
return { lines, logger: { info: (m) => lines.push(m), warn: (m) => lines.push(m) } };
|
|
70
|
+
};
|
|
71
|
+
/** A trust root shaped like one nobody configured. */
|
|
72
|
+
const unconfigured = { status: 'unconfigured', keys: {}, warnings: [] };
|
|
73
|
+
suite('lease trust root — sources and precedence', () => {
|
|
74
|
+
test('an explicit lease JWKS URL is used, and is marked as named outright', async () => {
|
|
75
|
+
let asked = '';
|
|
76
|
+
const root = await (0, signature_1.loadLeaseTrustRoot)({
|
|
77
|
+
env: { [signature_1.LEASE_JWKS_URL_ENV]: 'https://plane.example/.well-known/lease-jwks.json' },
|
|
78
|
+
leaseUrl: 'https://elsewhere.example/lease',
|
|
79
|
+
fetchImpl: (async (u) => { asked = u; return okResponse({ pemsByKid: { 'lease-2026': leaseKey.publicKeyPem } }); }),
|
|
80
|
+
});
|
|
81
|
+
assert.strictEqual(asked, 'https://plane.example/.well-known/lease-jwks.json');
|
|
82
|
+
assert.strictEqual(root.status, 'configured');
|
|
83
|
+
assert.strictEqual(root.explicitlyConfigured, true);
|
|
84
|
+
assert.deepStrictEqual(Object.keys(root.keys), ['lease-2026']);
|
|
85
|
+
});
|
|
86
|
+
test('inline keys need no network at all (air-gapped), same as the pack root', async () => {
|
|
87
|
+
const root = await (0, signature_1.loadLeaseTrustRoot)({
|
|
88
|
+
env: { [signature_1.LEASE_SIGNING_KEYS_ENV]: JSON.stringify({ 'lease-2026': leaseKey.publicKeyPem }) },
|
|
89
|
+
fetchImpl: (async () => { throw new Error('must not be called'); }),
|
|
90
|
+
});
|
|
91
|
+
assert.strictEqual(root.status, 'configured');
|
|
92
|
+
assert.strictEqual(root.explicitlyConfigured, true);
|
|
93
|
+
});
|
|
94
|
+
test('the derived URL asks for the lease-signing purpose, not the plane\'s default document', async () => {
|
|
95
|
+
let asked = '';
|
|
96
|
+
const root = await (0, signature_1.loadLeaseTrustRoot)({
|
|
97
|
+
env: {},
|
|
98
|
+
leaseUrl: 'https://plane.example/lease',
|
|
99
|
+
fetchImpl: (async (u) => { asked = u; return okResponse({ pemsByKid: { 'lease-2026': leaseKey.publicKeyPem } }); }),
|
|
100
|
+
});
|
|
101
|
+
// The bare path serves PACK-signing keys on a plane that splits by purpose. Deriving without
|
|
102
|
+
// the query string would hand the lease verifier a keyring that can never verify a lease —
|
|
103
|
+
// and, worse, one that can authorise code execution.
|
|
104
|
+
assert.strictEqual(asked, 'https://plane.example/.well-known/jwks.json?purpose=lease-signing');
|
|
105
|
+
assert.strictEqual(root.status, 'configured');
|
|
106
|
+
assert.strictEqual(root.derivedFromLease, true);
|
|
107
|
+
assert.strictEqual(root.explicitlyConfigured, false, 'derived is not "named outright"');
|
|
108
|
+
});
|
|
109
|
+
test('nothing configured at all is UNCONFIGURED, not an error', async () => {
|
|
110
|
+
const root = await (0, signature_1.loadLeaseTrustRoot)({ env: {} });
|
|
111
|
+
assert.strictEqual(root.status, 'unconfigured');
|
|
112
|
+
assert.deepStrictEqual(root.keys, {});
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
suite('lease trust root — the pack root\'s properties, unchanged', () => {
|
|
116
|
+
test('http is refused unless the documented escape hatch is set, and the hatch is LOUD', async () => {
|
|
117
|
+
const env = { [signature_1.LEASE_JWKS_URL_ENV]: 'http://plane.example/lease-jwks.json' };
|
|
118
|
+
const refused = await (0, signature_1.loadLeaseTrustRoot)({ env, fetchImpl: fetchReturning({ pemsByKid: { 'lease-2026': leaseKey.publicKeyPem } }) });
|
|
119
|
+
assert.strictEqual(refused.status, 'unavailable');
|
|
120
|
+
assert.match(refused.reason ?? '', /must be https/);
|
|
121
|
+
const allowed = await (0, signature_1.loadLeaseTrustRoot)({
|
|
122
|
+
env: { ...env, [signature_1.ALLOW_INSECURE_TRUST_ROOT_ENV]: '1' },
|
|
123
|
+
fetchImpl: fetchReturning({ pemsByKid: { 'lease-2026': leaseKey.publicKeyPem } }),
|
|
124
|
+
});
|
|
125
|
+
assert.strictEqual(allowed.status, 'configured');
|
|
126
|
+
assert.ok(allowed.warnings.some((w) => /INSECURE/.test(w)));
|
|
127
|
+
});
|
|
128
|
+
test('a fetch failure is UNAVAILABLE, never an empty keyring (finding A4)', async () => {
|
|
129
|
+
const root = await (0, signature_1.loadLeaseTrustRoot)({
|
|
130
|
+
env: { [signature_1.LEASE_JWKS_URL_ENV]: 'https://plane.example/j' },
|
|
131
|
+
fetchImpl: (async () => { throw new Error('ECONNREFUSED'); }),
|
|
132
|
+
});
|
|
133
|
+
assert.strictEqual(root.status, 'unavailable');
|
|
134
|
+
assert.match(root.reason ?? '', /ECONNREFUSED/);
|
|
135
|
+
// Still "named outright", so serve must NOT quietly fall back to the pack keyring here.
|
|
136
|
+
assert.strictEqual(root.explicitlyConfigured, true);
|
|
137
|
+
});
|
|
138
|
+
test('a garbled inline keyring names the variable an operator has to fix', async () => {
|
|
139
|
+
const root = await (0, signature_1.loadLeaseTrustRoot)({ env: { [signature_1.LEASE_SIGNING_KEYS_ENV]: '{not json' } });
|
|
140
|
+
assert.strictEqual(root.status, 'unavailable');
|
|
141
|
+
assert.match(root.reason ?? '', new RegExp(`${signature_1.LEASE_SIGNING_KEYS_ENV} is set but is not valid JSON`));
|
|
142
|
+
});
|
|
143
|
+
test('thumbprint pins refuse a key the plane did not previously publish', async () => {
|
|
144
|
+
const tp = (0, signature_1.publicKeyThumbprints)(leaseKey.publicKeyPem);
|
|
145
|
+
const root = await (0, signature_1.loadLeaseTrustRoot)({
|
|
146
|
+
env: { [signature_1.LEASE_JWKS_URL_ENV]: 'https://plane.example/j', [signature_1.LEASE_JWKS_PINS_ENV]: tp.spki },
|
|
147
|
+
fetchImpl: fetchReturning({ pemsByKid: { 'lease-2026': leaseKey.publicKeyPem, 'stranger': packKey.publicKeyPem } }),
|
|
148
|
+
});
|
|
149
|
+
assert.strictEqual(root.status, 'configured');
|
|
150
|
+
assert.deepStrictEqual(Object.keys(root.keys), ['lease-2026']);
|
|
151
|
+
assert.ok(root.warnings.some((w) => /lease trust root: key stranger does not match any pin/.test(w)));
|
|
152
|
+
});
|
|
153
|
+
test('a standard JWKS document (what the plane publishes) is accepted', async () => {
|
|
154
|
+
const root = await (0, signature_1.loadLeaseTrustRoot)({
|
|
155
|
+
env: { [signature_1.LEASE_JWKS_URL_ENV]: 'https://plane.example/j' },
|
|
156
|
+
fetchImpl: fetchReturning((0, warrant_1.jwks)([leaseKey])),
|
|
157
|
+
});
|
|
158
|
+
assert.strictEqual(root.status, 'configured');
|
|
159
|
+
assert.deepStrictEqual(Object.keys(root.keys), ['lease-2026']);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
suite('lease trust root — which keyring verifies a lease', () => {
|
|
163
|
+
test('a lease root named outright is the WHOLE answer: the pack keyring is not consulted', async () => {
|
|
164
|
+
const leaseTrust = await (0, signature_1.loadLeaseTrustRoot)({
|
|
165
|
+
env: { [signature_1.LEASE_SIGNING_KEYS_ENV]: JSON.stringify({ 'lease-2026': leaseKey.publicKeyPem }) },
|
|
166
|
+
});
|
|
167
|
+
const packTrust = await (0, signature_1.loadPackTrustRoot)({
|
|
168
|
+
env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://plane.example/j' },
|
|
169
|
+
fetchImpl: fetchReturning({ pemsByKid: { 'pack-2026': packKey.publicKeyPem } }),
|
|
170
|
+
});
|
|
171
|
+
const selected = (0, signature_1.selectLeaseSigningKeys)(leaseTrust, packTrust);
|
|
172
|
+
assert.deepStrictEqual(Object.keys(selected.keys), ['lease-2026']);
|
|
173
|
+
assert.strictEqual(selected.packFallback, false);
|
|
174
|
+
// A key that can vouch for a lease must not thereby be able to vouch for CODE, and the
|
|
175
|
+
// converse matters just as much: the pack key must stop being a lease key.
|
|
176
|
+
assert.ok(!('pack-2026' in selected.keys));
|
|
177
|
+
});
|
|
178
|
+
test('with no lease root named, the pack keyring stays in play and the caller is told to warn', async () => {
|
|
179
|
+
const packTrust = await (0, signature_1.loadPackTrustRoot)({
|
|
180
|
+
env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://plane.example/j' },
|
|
181
|
+
fetchImpl: fetchReturning({ pemsByKid: { 'pack-2026': packKey.publicKeyPem } }),
|
|
182
|
+
});
|
|
183
|
+
const selected = (0, signature_1.selectLeaseSigningKeys)(unconfigured, packTrust);
|
|
184
|
+
assert.strictEqual(selected.packFallback, true, 'the caller must warn, naming the new variable');
|
|
185
|
+
assert.deepStrictEqual(Object.keys(selected.keys), ['pack-2026']);
|
|
186
|
+
});
|
|
187
|
+
test('during a rollover both halves are live, and the lease key wins a kid collision', async () => {
|
|
188
|
+
const derived = { status: 'configured', keys: { shared: leaseKey.publicKeyPem }, warnings: [], derivedFromLease: true, explicitlyConfigured: false };
|
|
189
|
+
const packTrust = { status: 'configured', keys: { shared: packKey.publicKeyPem, 'pack-2026': packKey.publicKeyPem }, warnings: [] };
|
|
190
|
+
const selected = (0, signature_1.selectLeaseSigningKeys)(derived, packTrust);
|
|
191
|
+
assert.deepStrictEqual(Object.keys(selected.keys).sort(), ['pack-2026', 'shared']);
|
|
192
|
+
assert.strictEqual(selected.keys.shared, leaseKey.publicKeyPem);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
suite('signed leases end to end through the lease trust root', () => {
|
|
196
|
+
/** Exactly what serve wires: the lease source reads its keys through the selection above. */
|
|
197
|
+
const sourceOver = (leaseTrust, packTrust, body, logger) => (0, lease_1.createLeaseSource)({
|
|
198
|
+
url: 'https://plane.example/lease',
|
|
199
|
+
cacheSeconds: 0,
|
|
200
|
+
env: {},
|
|
201
|
+
instanceId: 'inst-1',
|
|
202
|
+
...(logger ? { logger } : {}),
|
|
203
|
+
signingKeys: () => (0, signature_1.selectLeaseSigningKeys)(leaseTrust, packTrust).keys,
|
|
204
|
+
fetchImpl: (async () => bodyResponse(body)),
|
|
205
|
+
});
|
|
206
|
+
test('a lease signed with the plane\'s lease-signing key verifies', async () => {
|
|
207
|
+
const leaseTrust = await (0, signature_1.loadLeaseTrustRoot)({
|
|
208
|
+
env: { [signature_1.LEASE_JWKS_URL_ENV]: 'https://plane.example/.well-known/lease-jwks.json' },
|
|
209
|
+
fetchImpl: fetchReturning((0, warrant_1.jwks)([leaseKey])),
|
|
210
|
+
});
|
|
211
|
+
const packTrust = await (0, signature_1.loadPackTrustRoot)({
|
|
212
|
+
env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://plane.example/j' },
|
|
213
|
+
fetchImpl: fetchReturning({ pemsByKid: { 'pack-2026': packKey.publicKeyPem } }),
|
|
214
|
+
});
|
|
215
|
+
const token = (0, lease_1.signLease)(lease, leaseKey, { instanceId: 'inst-1', nowSeconds: NOW });
|
|
216
|
+
const src = sourceOver(leaseTrust, packTrust, JSON.stringify({ lease: token }));
|
|
217
|
+
assert.deepStrictEqual(await src.get(NOW), lease);
|
|
218
|
+
assert.strictEqual(src.status(NOW).signed, true);
|
|
219
|
+
});
|
|
220
|
+
test('the back-compat path still verifies: a lease signed with the PACK key, no lease root named', async () => {
|
|
221
|
+
const packTrust = await (0, signature_1.loadPackTrustRoot)({
|
|
222
|
+
env: { [signature_1.PACK_JWKS_URL_ENV]: 'https://plane.example/j' },
|
|
223
|
+
fetchImpl: fetchReturning({ pemsByKid: { 'pack-2026': packKey.publicKeyPem } }),
|
|
224
|
+
});
|
|
225
|
+
const token = (0, lease_1.signLease)(lease, packKey, { instanceId: 'inst-1', nowSeconds: NOW });
|
|
226
|
+
const selected = (0, signature_1.selectLeaseSigningKeys)(unconfigured, packTrust);
|
|
227
|
+
assert.strictEqual(selected.packFallback, true, 'and the operator is told, once');
|
|
228
|
+
const src = sourceOver(unconfigured, packTrust, JSON.stringify({ lease: token }));
|
|
229
|
+
assert.deepStrictEqual(await src.get(NOW), lease, 'a deployment that works today must keep working');
|
|
230
|
+
});
|
|
231
|
+
test('a lease signed by a kid nobody trusts is refused', async () => {
|
|
232
|
+
const leaseTrust = await (0, signature_1.loadLeaseTrustRoot)({
|
|
233
|
+
env: { [signature_1.LEASE_SIGNING_KEYS_ENV]: JSON.stringify({ 'lease-2026': leaseKey.publicKeyPem }) },
|
|
234
|
+
});
|
|
235
|
+
const stranger = (0, warrant_1.generateWarrantKeypair)('stranger');
|
|
236
|
+
const { lines, logger } = collectLogs();
|
|
237
|
+
const src = sourceOver(leaseTrust, unconfigured, JSON.stringify({ lease: (0, lease_1.signLease)(lease, stranger, { nowSeconds: NOW }) }), logger);
|
|
238
|
+
assert.strictEqual(await src.get(NOW), undefined, 'nothing unverifiable is installed as policy');
|
|
239
|
+
assert.ok(lines.some((l) => /unknown kid/.test(l)));
|
|
240
|
+
});
|
|
241
|
+
test('a pack-key-signed lease is refused once a lease root IS named — silently trusting it would undo the split', async () => {
|
|
242
|
+
const leaseTrust = await (0, signature_1.loadLeaseTrustRoot)({
|
|
243
|
+
env: { [signature_1.LEASE_SIGNING_KEYS_ENV]: JSON.stringify({ 'lease-2026': leaseKey.publicKeyPem }) },
|
|
244
|
+
});
|
|
245
|
+
const packTrust = { status: 'configured', keys: { 'pack-2026': packKey.publicKeyPem }, warnings: [] };
|
|
246
|
+
const src = sourceOver(leaseTrust, packTrust, JSON.stringify({ lease: (0, lease_1.signLease)(lease, packKey, { nowSeconds: NOW }) }));
|
|
247
|
+
assert.strictEqual(await src.get(NOW), undefined);
|
|
248
|
+
});
|
|
249
|
+
test('fail-open on a SIGNATURE failure survives: the last good lease stands', async () => {
|
|
250
|
+
const leaseTrust = await (0, signature_1.loadLeaseTrustRoot)({
|
|
251
|
+
env: { [signature_1.LEASE_SIGNING_KEYS_ENV]: JSON.stringify({ 'lease-2026': leaseKey.publicKeyPem }) },
|
|
252
|
+
});
|
|
253
|
+
const good = (0, lease_1.signLease)(lease, leaseKey, { instanceId: 'inst-1', nowSeconds: NOW });
|
|
254
|
+
const forged = (0, lease_1.signLease)({ ...lease, enabled: false, version: '9.9.9' }, (0, warrant_1.generateWarrantKeypair)('stranger'), { nowSeconds: NOW + 1 });
|
|
255
|
+
let phase = 'good';
|
|
256
|
+
const { lines, logger } = collectLogs();
|
|
257
|
+
const src = (0, lease_1.createLeaseSource)({
|
|
258
|
+
url: 'https://plane.example/lease',
|
|
259
|
+
cacheSeconds: 0,
|
|
260
|
+
env: {},
|
|
261
|
+
instanceId: 'inst-1',
|
|
262
|
+
logger,
|
|
263
|
+
signingKeys: () => (0, signature_1.selectLeaseSigningKeys)(leaseTrust, unconfigured).keys,
|
|
264
|
+
fetchImpl: (async () => bodyResponse(JSON.stringify({ lease: phase === 'good' ? good : forged }))),
|
|
265
|
+
});
|
|
266
|
+
assert.deepStrictEqual(await src.get(NOW), lease);
|
|
267
|
+
phase = 'forged';
|
|
268
|
+
// An unverifiable document is treated exactly like a transport failure — never installed as
|
|
269
|
+
// policy, and never allowed to take a fleet down by being publishable junk.
|
|
270
|
+
assert.deepStrictEqual(await src.get(NOW + 1), lease);
|
|
271
|
+
assert.ok(lines.some((l) => /fetch failed/.test(l)));
|
|
272
|
+
});
|
|
273
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* "Junk is dropped rather than trusted" — restored under the sandbox vocabulary.
|
|
4
|
+
*
|
|
5
|
+
* `normalizeCaps` was the ONLY runtime validation a lease ever received, and it was deleted along
|
|
6
|
+
* with the cell→sandbox back-compat it happened to also carry. What replaced it was
|
|
7
|
+
* `cached = (await res.json()) as Lease` — a compile-time cast that checks nothing. These pin the
|
|
8
|
+
* properties that were lost, plus the one that was always wrong: an unrecognised breach action
|
|
9
|
+
* must fail closed to the STRICTER action, never silently to the weaker one.
|
|
10
|
+
*--------------------------------------------------------------------------------------------*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
require("mocha");
|
|
46
|
+
const assert = __importStar(require("assert"));
|
|
47
|
+
const caps_1 = require("../pack/caps");
|
|
48
|
+
const caps_2 = require("../pack/caps");
|
|
49
|
+
const lease_schema_1 = require("../pack/lease-schema");
|
|
50
|
+
const base = { pack: 'analyst', enabled: true, expiresAt: 1_800_003_600 };
|
|
51
|
+
suite('lease validation — structural junk is refused, not trusted', () => {
|
|
52
|
+
const rejected = [
|
|
53
|
+
['a string cap', { ...base, caps: { maxTurnsPerSession: 'lots' } }],
|
|
54
|
+
['a negative cap', { ...base, caps: { maxSessionSeconds: -1 } }],
|
|
55
|
+
['a zero cap', { ...base, caps: { maxConcurrentSandboxes: 0 } }],
|
|
56
|
+
['a fractional cap', { ...base, caps: { maxTurnsPerSession: 1.5 } }],
|
|
57
|
+
['an infinite cap', { ...base, caps: { maxTurnsPerSession: Number.POSITIVE_INFINITY } }],
|
|
58
|
+
['a string enabled', { ...base, enabled: 'false' }],
|
|
59
|
+
['a missing pack', { enabled: true, expiresAt: 1 }],
|
|
60
|
+
['a string expiresAt', { ...base, expiresAt: '1800003600' }],
|
|
61
|
+
['a non-object caps', { ...base, caps: 'none' }],
|
|
62
|
+
['a non-array deny', { ...base, deny: { scope: 'pack' } }],
|
|
63
|
+
['an unknown deny scope', { ...base, deny: [{ scope: 'tenant', match: 'x', mode: 'kill' }] }],
|
|
64
|
+
['a deny rule with no match', { ...base, deny: [{ scope: 'pack', match: '', mode: 'kill' }] }],
|
|
65
|
+
['a non-boolean requireSignedPack', { ...base, requireSignedPack: 'yes' }],
|
|
66
|
+
['not an object at all', 'nope'],
|
|
67
|
+
];
|
|
68
|
+
for (const [name, doc] of rejected) {
|
|
69
|
+
test(`${name} rejects the whole document`, () => {
|
|
70
|
+
const res = (0, lease_schema_1.parseLease)(doc);
|
|
71
|
+
assert.strictEqual(res.ok, false, `expected ${name} to be refused`);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
test('NaN never reaches the meter, where every comparison against it is false', () => {
|
|
75
|
+
// The failure this prevents: `usage.turns >= NaN` is false for every value, so the cap is
|
|
76
|
+
// reported in the console and enforced nowhere.
|
|
77
|
+
assert.strictEqual((0, lease_schema_1.parseLease)({ ...base, caps: { maxTurnsPerSession: NaN } }).ok, false);
|
|
78
|
+
const meter = new caps_2.CapMeter({ maxTurnsPerSession: NaN });
|
|
79
|
+
meter.record('s1');
|
|
80
|
+
meter.record('s1');
|
|
81
|
+
meter.record('s1');
|
|
82
|
+
assert.strictEqual(meter.check('s1', 0).allowed, true, 'demonstrating why the validator has to catch it');
|
|
83
|
+
});
|
|
84
|
+
test('a well-formed lease survives intact', () => {
|
|
85
|
+
const res = (0, lease_schema_1.parseLease)({
|
|
86
|
+
...base,
|
|
87
|
+
version: '1.4.1',
|
|
88
|
+
caps: { maxTurnsPerSession: 10, onBreach: 'destroy-sandbox' },
|
|
89
|
+
deny: [{ scope: 'session', match: 's1', mode: 'kill', reason: 'runaway', until: 1_800_004_000 }],
|
|
90
|
+
requireSignedPack: true,
|
|
91
|
+
});
|
|
92
|
+
assert.ok(res.ok);
|
|
93
|
+
assert.strictEqual(res.lease.caps?.maxTurnsPerSession, 10);
|
|
94
|
+
assert.strictEqual(res.lease.deny?.[0].mode, 'kill');
|
|
95
|
+
assert.strictEqual(res.lease.requireSignedPack, true);
|
|
96
|
+
});
|
|
97
|
+
test('an unknown cap field is tolerated but never silent', () => {
|
|
98
|
+
const res = (0, lease_schema_1.parseCapPolicy)({ maxSomethingNew: 5 });
|
|
99
|
+
assert.ok(res.ok);
|
|
100
|
+
assert.ok(res.warnings.some((w) => /NOT being enforced/.test(w)));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
suite('unrecognised enums fail CLOSED to the stricter value', () => {
|
|
104
|
+
/**
|
|
105
|
+
* The concrete bug: an unrecognised `onBreach` fell through `mergeCaps`'s
|
|
106
|
+
* `=== 'destroy-sandbox'` test, so `governor.enforce` defaulted to `revoke-inference`. A typo
|
|
107
|
+
* in the control plane SOFTENED the breach action from "destroy the session" to "take away its
|
|
108
|
+
* model route", while the console displayed the stricter value the operator had entered.
|
|
109
|
+
*/
|
|
110
|
+
test('an unknown lease onBreach clamps UP and warns', () => {
|
|
111
|
+
const res = (0, lease_schema_1.parseCapPolicy)({ onBreach: 'destroy_cell_typo' });
|
|
112
|
+
assert.ok(res.ok);
|
|
113
|
+
assert.strictEqual(res.caps.onBreach, lease_schema_1.STRICTEST_ON_BREACH);
|
|
114
|
+
assert.strictEqual(res.caps.onBreach, 'destroy-sandbox');
|
|
115
|
+
assert.ok(res.warnings.some((w) => /Failing CLOSED/.test(w)));
|
|
116
|
+
});
|
|
117
|
+
test('and the clamped value survives mergeCaps rather than being softened by it', () => {
|
|
118
|
+
const parsed = (0, lease_schema_1.parseCapPolicy)({ onBreach: 'nonsense' });
|
|
119
|
+
assert.ok(parsed.ok);
|
|
120
|
+
assert.strictEqual((0, caps_1.mergeCaps)(parsed.caps, { onBreach: 'revoke-inference' }).onBreach, 'destroy-sandbox');
|
|
121
|
+
});
|
|
122
|
+
test('an unknown deny mode clamps to kill rather than being dropped', () => {
|
|
123
|
+
const res = (0, lease_schema_1.parseLease)({ ...base, deny: [{ scope: 'pack', match: 'analyst', mode: 'pause' }] });
|
|
124
|
+
assert.ok(res.ok);
|
|
125
|
+
assert.strictEqual(res.lease.deny?.[0].mode, 'kill');
|
|
126
|
+
assert.ok(res.warnings.some((w) => /Failing CLOSED/.test(w)));
|
|
127
|
+
});
|
|
128
|
+
test('env: an unknown AGENTHIPPO_CAP_ON_BREACH fails closed instead of becoming undefined', () => {
|
|
129
|
+
const warnings = [];
|
|
130
|
+
const caps = (0, caps_1.capsFromEnv)({ AGENTHIPPO_CAP_ON_BREACH: 'destroy-everything' }, (m) => warnings.push(m));
|
|
131
|
+
assert.strictEqual(caps.onBreach, 'destroy-sandbox');
|
|
132
|
+
assert.ok(warnings.some((w) => /FAILING CLOSED/.test(w)));
|
|
133
|
+
});
|
|
134
|
+
test('CLI: the same resolver, so a flag and an env var cannot disagree', () => {
|
|
135
|
+
const r = (0, caps_1.resolveOnBreach)('destroy-everything', '--cap-on-breach');
|
|
136
|
+
assert.strictEqual(r.action, 'destroy-sandbox');
|
|
137
|
+
assert.ok(r.warnings.some((w) => /FAILING CLOSED/.test(w)));
|
|
138
|
+
assert.strictEqual((0, caps_1.resolveOnBreach)(undefined, '--cap-on-breach').action, undefined, 'unset stays unset');
|
|
139
|
+
assert.strictEqual((0, caps_1.resolveOnBreach)('revoke-inference', '--cap-on-breach').action, 'revoke-inference');
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
suite('deprecated cell vocabulary is aliased, loudly', () => {
|
|
143
|
+
test('destroy-cell maps to destroy-sandbox rather than softening enforcement', () => {
|
|
144
|
+
assert.strictEqual(lease_schema_1.ON_BREACH_ALIASES['destroy-cell'], 'destroy-sandbox');
|
|
145
|
+
const r = (0, caps_1.resolveOnBreach)('destroy-cell', 'AGENTHIPPO_CAP_ON_BREACH');
|
|
146
|
+
assert.strictEqual(r.action, 'destroy-sandbox');
|
|
147
|
+
assert.ok(r.warnings.some((w) => /DEPRECATED/.test(w)));
|
|
148
|
+
});
|
|
149
|
+
test('a lease using the old cap spelling is still enforced', () => {
|
|
150
|
+
assert.strictEqual(lease_schema_1.LEASE_CAP_ALIASES.maxConcurrentCells, 'maxConcurrentSandboxes');
|
|
151
|
+
const res = (0, lease_schema_1.parseCapPolicy)({ maxConcurrentCells: 4 });
|
|
152
|
+
assert.ok(res.ok);
|
|
153
|
+
assert.strictEqual(res.caps.maxConcurrentSandboxes, 4);
|
|
154
|
+
});
|
|
155
|
+
test('AGENTHIPPO_MAX_CONCURRENT_CELLS is honoured with a deprecation notice', () => {
|
|
156
|
+
const warnings = [];
|
|
157
|
+
const caps = (0, caps_1.capsFromEnv)({ AGENTHIPPO_MAX_CONCURRENT_CELLS: '3' }, (m) => warnings.push(m));
|
|
158
|
+
assert.strictEqual(caps.maxConcurrentSandboxes, 3);
|
|
159
|
+
assert.ok(warnings.some((w) => /DEPRECATED/.test(w)));
|
|
160
|
+
assert.strictEqual(caps_1.CAP_ENV_ALIASES.AGENTHIPPO_MAX_CONCURRENT_CELLS, 'AGENTHIPPO_MAX_CONCURRENT_SANDBOXES');
|
|
161
|
+
});
|
|
162
|
+
test('the current spelling wins when both are present', () => {
|
|
163
|
+
const caps = (0, caps_1.capsFromEnv)({
|
|
164
|
+
AGENTHIPPO_MAX_CONCURRENT_SANDBOXES: '7',
|
|
165
|
+
AGENTHIPPO_MAX_CONCURRENT_CELLS: '3',
|
|
166
|
+
}, () => undefined);
|
|
167
|
+
assert.strictEqual(caps.maxConcurrentSandboxes, 7);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Lease admission — kill switch + rollback (PLAN/serve-v4-clean.md §5).
|
|
4
|
+
*
|
|
5
|
+
* One mechanism, two jobs. The tests pin the operational contract, especially the
|
|
6
|
+
* fail-open-on-transport / fail-closed-on-content asymmetry, which is exactly the kind of
|
|
7
|
+
* property that silently inverts during a refactor.
|
|
8
|
+
*--------------------------------------------------------------------------------------------*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
require("mocha");
|
|
44
|
+
const assert = __importStar(require("assert"));
|
|
45
|
+
const lease_1 = require("../pack/lease");
|
|
46
|
+
const NOW = 1_800_000_000;
|
|
47
|
+
const valid = { pack: 'analyst', expiresAt: NOW + 3600, enabled: true };
|
|
48
|
+
suite('lease admission — kill switch', () => {
|
|
49
|
+
test('an enabled, unexpired lease admits', () => {
|
|
50
|
+
assert.doesNotThrow(() => (0, lease_1.assertAdmitted)(valid, { pack: 'analyst', version: '1.4.2', nowSeconds: NOW }));
|
|
51
|
+
});
|
|
52
|
+
test('no lease configured admits — self-hosted dev and free tier keep working', () => {
|
|
53
|
+
assert.doesNotThrow(() => (0, lease_1.assertAdmitted)(undefined, { pack: 'analyst', version: '1.4.2', nowSeconds: NOW }));
|
|
54
|
+
});
|
|
55
|
+
test('the kill switch refuses new sandboxes immediately, with the operator reason', () => {
|
|
56
|
+
assert.throws(() => (0, lease_1.assertAdmitted)({ ...valid, enabled: false, reason: 'incident 4412' }, { pack: 'analyst', version: '1.4.2', nowSeconds: NOW }), (e) => e instanceof lease_1.LeaseDeniedError && /kill switch active/.test(e.message) && /incident 4412/.test(e.message));
|
|
57
|
+
});
|
|
58
|
+
test('an expired lease is refused — fail closed on content', () => {
|
|
59
|
+
assert.throws(() => (0, lease_1.assertAdmitted)({ ...valid, expiresAt: NOW - 1 }, { pack: 'analyst', version: '1.4.2', nowSeconds: NOW }), /expired/);
|
|
60
|
+
});
|
|
61
|
+
test("a lease for another pack does not admit this one", () => {
|
|
62
|
+
assert.throws(() => (0, lease_1.assertAdmitted)({ ...valid, pack: 'other' }, { pack: 'analyst', version: '1.4.2', nowSeconds: NOW }), /lease is for pack other/);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
suite('lease admission — rollback', () => {
|
|
66
|
+
test('a pinned version refuses any other version (rollback in force)', () => {
|
|
67
|
+
const pinned = { ...valid, version: '1.4.1' };
|
|
68
|
+
assert.doesNotThrow(() => (0, lease_1.assertAdmitted)(pinned, { pack: 'analyst', version: '1.4.1', nowSeconds: NOW }));
|
|
69
|
+
assert.throws(() => (0, lease_1.assertAdmitted)(pinned, { pack: 'analyst', version: '1.4.2', nowSeconds: NOW }), /pinned to 1\.4\.1.*refusing to start 1\.4\.2/);
|
|
70
|
+
});
|
|
71
|
+
test('an image that does not match the leased digest is refused (supply-chain pin)', () => {
|
|
72
|
+
const pinned = { ...valid, imageDigest: 'sha256:' + 'a'.repeat(64) };
|
|
73
|
+
assert.doesNotThrow(() => (0, lease_1.assertAdmitted)(pinned, {
|
|
74
|
+
pack: 'analyst', version: '1.4.2', image: `ghcr.io/x/analyst@sha256:${'a'.repeat(64)}`, nowSeconds: NOW,
|
|
75
|
+
}));
|
|
76
|
+
assert.throws(() => (0, lease_1.assertAdmitted)(pinned, {
|
|
77
|
+
pack: 'analyst', version: '1.4.2', image: `ghcr.io/x/analyst@sha256:${'b'.repeat(64)}`, nowSeconds: NOW,
|
|
78
|
+
}), /does not match the leased digest/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
suite('lease source — fail-open transport, fail-closed content', () => {
|
|
82
|
+
test('a network failure keeps serving on the cached lease', async () => {
|
|
83
|
+
let calls = 0;
|
|
84
|
+
const src = (0, lease_1.createLeaseSource)({
|
|
85
|
+
url: 'http://control.invalid/lease',
|
|
86
|
+
cacheSeconds: 0,
|
|
87
|
+
fetchImpl: (async () => {
|
|
88
|
+
calls++;
|
|
89
|
+
if (calls === 1) {
|
|
90
|
+
return { ok: true, json: async () => valid };
|
|
91
|
+
}
|
|
92
|
+
throw new Error('control plane unreachable');
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
95
|
+
assert.deepStrictEqual(await src.get(NOW), valid, 'first fetch populates the cache');
|
|
96
|
+
const during = await src.get(NOW + 1);
|
|
97
|
+
assert.deepStrictEqual(during, valid, 'an outage must not take a customer fleet down');
|
|
98
|
+
});
|
|
99
|
+
test('an outage cannot extend an entitlement — the cached lease still expires', async () => {
|
|
100
|
+
const shortLease = { pack: 'analyst', expiresAt: NOW + 10, enabled: true };
|
|
101
|
+
const src = (0, lease_1.createLeaseSource)({
|
|
102
|
+
url: 'http://control.invalid/lease',
|
|
103
|
+
cacheSeconds: 0,
|
|
104
|
+
fetchImpl: (async () => ({ ok: true, json: async () => shortLease })),
|
|
105
|
+
});
|
|
106
|
+
const lease = await src.get(NOW);
|
|
107
|
+
// Later, past its own expiry, the cached lease must still be refused by admission.
|
|
108
|
+
assert.throws(() => (0, lease_1.assertAdmitted)(lease, { pack: 'analyst', version: '1.4.2', nowSeconds: NOW + 11 }), /expired/);
|
|
109
|
+
});
|
|
110
|
+
test('a static lease works with no control plane at all (air-gapped)', async () => {
|
|
111
|
+
const src = (0, lease_1.createLeaseSource)({ staticLease: valid });
|
|
112
|
+
assert.deepStrictEqual(await src.get(NOW), valid);
|
|
113
|
+
});
|
|
114
|
+
});
|
package/out/test/mcp.test.js
CHANGED
|
@@ -331,7 +331,7 @@ suite('MCP Server Loading', () => {
|
|
|
331
331
|
remote: { type: 'http', url: 'https://mcp.example.com/sse', headers: { Authorization: 'Bearer abc' } },
|
|
332
332
|
});
|
|
333
333
|
});
|
|
334
|
-
test('bearerTokenEnv resolves against the PER-TURN env into an Authorization header (
|
|
334
|
+
test('bearerTokenEnv resolves against the PER-TURN env into an Authorization header (cursor/custom path)', () => {
|
|
335
335
|
const server = {
|
|
336
336
|
name: 'data', label: 'data', command: '', args: [],
|
|
337
337
|
url: 'http://127.0.0.1:39999/mcp', bearerTokenEnv: 'AGENTHIPPO_TURN_NONCE',
|
|
@@ -346,6 +346,32 @@ suite('MCP Server Loading', () => {
|
|
|
346
346
|
data: { type: 'http', url: 'http://127.0.0.1:39999/mcp' },
|
|
347
347
|
});
|
|
348
348
|
});
|
|
349
|
+
/**
|
|
350
|
+
* The claude path must ship a `${NAME}` REFERENCE, never the resolved secret: the Agent
|
|
351
|
+
* SDK serializes mcpServers into `--mcp-config <json>` argv, and /proc/<pid>/cmdline is
|
|
352
|
+
* world-readable — a resolved bearer there leaks the turn nonce to every uid on the host.
|
|
353
|
+
* The CLI expands the reference from its own env (verified live on 2.1.220).
|
|
354
|
+
*/
|
|
355
|
+
test('referenceHttpMcpHeaders emits a ${NAME} reference, never the secret (claude path)', () => {
|
|
356
|
+
const headers = (0, mcp_1.referenceHttpMcpHeaders)({ bearerTokenEnv: 'AGENTHIPPO_TURN_NONCE', headers: { 'X-Static': 'kept' } }, { AGENTHIPPO_TURN_NONCE: 'nonce-123' });
|
|
357
|
+
assert.deepStrictEqual(headers, {
|
|
358
|
+
'X-Static': 'kept',
|
|
359
|
+
'Authorization': 'Bearer ${AGENTHIPPO_TURN_NONCE}',
|
|
360
|
+
});
|
|
361
|
+
// The reference is emitted even when the env has no value yet: expansion happens in
|
|
362
|
+
// the CLI at connect time, and an unset name passes through literally, which the data
|
|
363
|
+
// gateway rejects — fail closed, never a silently-authorized request.
|
|
364
|
+
const unresolved = (0, mcp_1.referenceHttpMcpHeaders)({ bearerTokenEnv: 'AGENTHIPPO_TURN_NONCE' }, {});
|
|
365
|
+
assert.deepStrictEqual(unresolved, { Authorization: 'Bearer ${AGENTHIPPO_TURN_NONCE}' });
|
|
366
|
+
});
|
|
367
|
+
test('referenceHttpMcpHeaders falls back to in-memory resolution for a non-expandable name', () => {
|
|
368
|
+
// A name outside the CLI's `${NAME}` grammar would be sent verbatim as garbage; the
|
|
369
|
+
// fallback resolves it here instead, accepting environ-level exposure over a broken header.
|
|
370
|
+
const headers = (0, mcp_1.referenceHttpMcpHeaders)({ bearerTokenEnv: 'weird-name.v2' }, { 'weird-name.v2': 'tok-1' });
|
|
371
|
+
assert.deepStrictEqual(headers, { Authorization: 'Bearer tok-1' });
|
|
372
|
+
// No bearerTokenEnv and no static headers → no headers object at all.
|
|
373
|
+
assert.strictEqual((0, mcp_1.referenceHttpMcpHeaders)({}, {}), undefined);
|
|
374
|
+
});
|
|
349
375
|
test('serialize and parse round-trip inline MCP', () => {
|
|
350
376
|
const inline = (0, mcp_1.formatInlineMcpServers)([
|
|
351
377
|
{ name: 'echo', label: 'echo', command: 'echo', args: ['hi'] },
|