@agenthippo-ai/agenthippo 1.110.9 → 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/service.js +6 -0
- package/out/analytics/sessionEvents.js +4 -3
- package/out/auth-broker/server.js +23 -19
- package/out/cli/commands/data-plane.js +84 -0
- package/out/cli/commands/sandbox.js +28 -9
- package/out/cli/commands/serve.js +289 -97
- package/out/cli/credential-context.js +17 -0
- package/out/cli/data-plane/index.js +2 -1
- package/out/cli/data-plane/sidecar.js +66 -2
- package/out/cli/deploy/layers/identity.js +5 -2
- package/out/cli/deploy-init.js +4 -4
- package/out/cli/index.js +16 -1
- package/out/cli/serve-hardening.js +181 -12
- package/out/engines/claude.js +1 -1
- package/out/engines/codex.js +1 -1
- package/out/engines/custom.js +7 -1
- package/out/evidence/sessionShip.js +50 -17
- package/out/headless/index.js +8 -1
- package/out/pack/governor.js +32 -15
- package/out/pack/lease-schema.js +4 -1
- package/out/pack/lease.js +42 -0
- package/out/runner/host.js +24 -14
- package/out/runner/protocol.js +24 -4
- package/out/runner/remoteEngine.js +53 -17
- package/out/sandbox/containerDriver.js +4 -2
- package/out/sandbox/processDriver.js +4 -1
- package/out/sandbox/sandboxRunner.js +148 -36
- package/out/scripts/agentide_litellm_hooks_v1.py +85 -1
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +4 -0
- package/out/scripts/analytics/setup-analytics.ps1 +8 -1
- package/out/scripts/analytics/setup-analytics.sh +13 -2
- package/out/test/caps-governor.test.js +6 -1
- package/out/test/data-plane-engine-wiring.test.js +32 -0
- package/out/test/data-plane.test.js +313 -0
- package/out/test/deploy-init.test.js +7 -1
- package/out/test/governor-hardening.test.js +6 -1
- package/out/test/lease-validation.test.js +55 -0
- package/out/test/localFleetAdapter.test.js +91 -0
- package/out/test/sandbox-driver.test.js +2 -2
- package/out/test/sandbox-hardening.test.js +12 -1
- package/out/test/sandbox-runner.test.js +1 -1
- 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 +182 -9
- package/out/test/sessionShip.test.js +58 -0
- package/package.json +1 -1
|
@@ -39,6 +39,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
39
39
|
exports.serveCommand = serveCommand;
|
|
40
40
|
exports.resolveGatewaySessionKey = resolveGatewaySessionKey;
|
|
41
41
|
exports.runGatewayTurn = runGatewayTurn;
|
|
42
|
+
exports.runBridgeTurn = runBridgeTurn;
|
|
42
43
|
/**
|
|
43
44
|
* Serve Command - HTTP Server
|
|
44
45
|
*/
|
|
@@ -66,6 +67,8 @@ const pack_runtime_1 = require("../pack-runtime");
|
|
|
66
67
|
const sessionShip_1 = require("../../evidence/sessionShip");
|
|
67
68
|
const serve_hardening_1 = require("../serve-hardening");
|
|
68
69
|
const credential_context_1 = require("../credential-context");
|
|
70
|
+
const lease_2 = require("../../pack/lease");
|
|
71
|
+
const identity_1 = require("../../analytics/identity");
|
|
69
72
|
const data_plane_1 = require("../data-plane");
|
|
70
73
|
const runtime_1 = require("../runtime");
|
|
71
74
|
const outboundSend_1 = require("../../gateway/outboundSend");
|
|
@@ -221,6 +224,25 @@ async function serveCommand(options) {
|
|
|
221
224
|
for (const line of (0, serve_hardening_1.identityHeaderAdvice)(process.env, bindHost)) {
|
|
222
225
|
console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${line}`);
|
|
223
226
|
}
|
|
227
|
+
// A pinned header means multiple users; a tier-0 or sandbox-less serve means one filesystem for
|
|
228
|
+
// all of them. That pairing had no boot-time signal at all, while the strictly SAFER container
|
|
229
|
+
// case (--shared-workspace) has warned since it shipped.
|
|
230
|
+
const tenantIsolation = (0, serve_hardening_1.tenantIsolationAdvice)({
|
|
231
|
+
sandbox: options.sandbox,
|
|
232
|
+
sharedWorkspace: Boolean(options.sharedWorkspace),
|
|
233
|
+
workspace: options.workspace,
|
|
234
|
+
stateDir: storagePath,
|
|
235
|
+
});
|
|
236
|
+
if (tenantIsolation.level === 'refuse') {
|
|
237
|
+
for (const line of tenantIsolation.lines) {
|
|
238
|
+
console.error(`${utils_1.COLORS.red}Error:${utils_1.COLORS.reset} ${line}`);
|
|
239
|
+
}
|
|
240
|
+
console.error(`Refusing to start because ${serve_hardening_1.REQUIRE_TENANT_ISOLATION_ENV} is set. Unset it to downgrade this to a warning.`);
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
for (const line of tenantIsolation.lines) {
|
|
244
|
+
console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${line}`);
|
|
245
|
+
}
|
|
224
246
|
// Passed to createPackRunner below so the runner's own SessionEventWriter uses the same
|
|
225
247
|
// root — a fail-closed warrant denial lands in the same per-session JSONL as the tool events
|
|
226
248
|
// it precedes. The denial happens before runner.run() is ever called, so it needs its own
|
|
@@ -248,7 +270,26 @@ async function serveCommand(options) {
|
|
|
248
270
|
// One shared registry: a single per-turn nonce grants BOTH the data-plane and egress
|
|
249
271
|
// capabilities (secure-obo-design §7.6 step 2). Created when either LOCAL broker will run.
|
|
250
272
|
const turnRegistry = ((dataPlaneCfg && !remoteDataPlane && !edgeDataPlane) || options.gatewayBridge) ? new data_plane_1.TurnRegistry() : null;
|
|
251
|
-
|
|
273
|
+
/**
|
|
274
|
+
* Every data-plane audit row carries BOTH principals (PLAN/agent-identity.md §1 failure 3,
|
|
275
|
+
* §10).
|
|
276
|
+
*
|
|
277
|
+
* The user dimension was already on these rows. The agent was not, so evidence could answer
|
|
278
|
+
* "which user touched which rows" but not "via which agent" — the auditor's second question,
|
|
279
|
+
* and the one the whole design exists to make answerable. It was only ever *implied*, by which
|
|
280
|
+
* deployment happened to write the file, and that implication breaks entirely the moment two
|
|
281
|
+
* serves share a connector (§4.4).
|
|
282
|
+
*
|
|
283
|
+
* Read at emit time, not captured once: `activeVersion` and `packDigestValue` both change on a
|
|
284
|
+
* pack hot-swap, and a row must name the pack that actually served it.
|
|
285
|
+
*
|
|
286
|
+
* The digest is carried in full rather than truncated — it is not a secret, and a prefix cannot
|
|
287
|
+
* be checked against a signed artifact, which is the entire point of recording it.
|
|
288
|
+
*/
|
|
289
|
+
const auditEvent = (sessionKey, event, fields) => sessionEventWriter.append(sessionKey, (0, base_1.generateRequestId)(), event, {
|
|
290
|
+
...fields,
|
|
291
|
+
agent: { pack: packId, version: activeVersion, digest: packDigestValue ?? null },
|
|
292
|
+
});
|
|
252
293
|
let dataPlane = null;
|
|
253
294
|
if (edgeDataPlane) {
|
|
254
295
|
process.env[data_plane_1.DATA_MCP_URL_ENV] = edgeDataPlane.url;
|
|
@@ -317,7 +358,13 @@ async function serveCommand(options) {
|
|
|
317
358
|
}
|
|
318
359
|
// Sidecar: register with the connector container; serve never sees a base credential.
|
|
319
360
|
if (remoteDataPlane) {
|
|
320
|
-
|
|
361
|
+
// Name the agent on every registration (PR-8). Asserted, not proven — the sidecar
|
|
362
|
+
// records it and flags a mismatch against the pack it loaded, which is how "one
|
|
363
|
+
// connector, one agent" (§4.4) becomes observable rather than merely assumed.
|
|
364
|
+
const r = await remoteDataPlane.registerTurn({
|
|
365
|
+
...reg,
|
|
366
|
+
agent: { pack: packId, version: activeVersion, digest: packDigestValue },
|
|
367
|
+
});
|
|
321
368
|
const env = { ...r.env };
|
|
322
369
|
if (egressBroker) {
|
|
323
370
|
env[data_plane_1.EGRESS_URL_ENV] = egressBroker.url;
|
|
@@ -708,6 +755,9 @@ async function serveCommand(options) {
|
|
|
708
755
|
// next turn. That silently contradicts "one disposable sandbox per session" for everything that
|
|
709
756
|
// matters, since the container boundary does not extend to a directory both containers mount.
|
|
710
757
|
const sharedWorkspace = Boolean(options.sharedWorkspace);
|
|
758
|
+
// One sandbox per conversation unless told otherwise. Every key below derives from this, and
|
|
759
|
+
// setting it back to `session` is the whole rollback.
|
|
760
|
+
const sandboxScope = options.sandboxScope === 'user' ? 'user' : 'session';
|
|
711
761
|
const sandboxMounts = options.sandbox === 'container'
|
|
712
762
|
? {
|
|
713
763
|
// serve's resolved pack, not the image's baked copy — the two cannot then disagree
|
|
@@ -734,25 +784,31 @@ async function serveCommand(options) {
|
|
|
734
784
|
});
|
|
735
785
|
sandboxImageUser = await (0, support_1.resolveSandboxImageUser)(support_1.dockerExec, options.sandboxImage);
|
|
736
786
|
}
|
|
737
|
-
//
|
|
738
|
-
// sandbox is reaped (serve-v4-sandbox §8.4).
|
|
739
|
-
//
|
|
740
|
-
|
|
787
|
+
// Engine state must outlive the sandboxes that use it or the conversation restarts every time a
|
|
788
|
+
// sandbox is reaped (serve-v4-sandbox §8.4). Keyed at SANDBOX scope, under serve's own state
|
|
789
|
+
// dir: one directory per session by default, or one per user under `--sandbox-scope user`,
|
|
790
|
+
// where the sessions sharing it stay separate beneath in `provider-sessions/`.
|
|
791
|
+
const sandboxStateDirFor = (sandboxKey) => (0, support_1.ensureSandboxStateDir)(path.join(storagePath, 'sandbox-state', (0, state_sync_1.safeSessionId)(sandboxKey)), sandboxImageUser);
|
|
741
792
|
/**
|
|
742
|
-
* Mounts for ONE
|
|
793
|
+
* Mounts for ONE sandbox: the pack read-only, and a workspace only that sandbox sees.
|
|
743
794
|
*
|
|
744
|
-
* The
|
|
745
|
-
*
|
|
746
|
-
*
|
|
795
|
+
* The workspace lives beside the engine state it belongs to, so the same reap/prune machinery
|
|
796
|
+
* already covers it and the agent's output stays visible on the host — the property the shared
|
|
797
|
+
* mount was added for — without being visible to every other sandbox.
|
|
798
|
+
*
|
|
799
|
+
* Keyed at sandbox scope, never narrower: the in-sandbox host chdir's here on every turn, and
|
|
800
|
+
* that is only safe while every session sharing a sandbox resolves the same directory (see the
|
|
801
|
+
* INVARIANT note in cli/commands/sandbox.ts). Under user scope this means a user's own sessions
|
|
802
|
+
* share a workspace — same trust domain, on the same terms their engine homes already shared.
|
|
747
803
|
*/
|
|
748
|
-
const sandboxFsFor = async (
|
|
804
|
+
const sandboxFsFor = async (sandboxKey) => {
|
|
749
805
|
if (!sandboxMounts) {
|
|
750
806
|
return { readonly: [], writable: [] };
|
|
751
807
|
}
|
|
752
808
|
if (sharedWorkspace) {
|
|
753
809
|
return sandboxMounts;
|
|
754
810
|
}
|
|
755
|
-
const dir = (0, support_1.ensureSandboxStateDir)(path.join(storagePath, 'sandbox-workspace', (0, state_sync_1.safeSessionId)(
|
|
811
|
+
const dir = (0, support_1.ensureSandboxStateDir)(path.join(storagePath, 'sandbox-workspace', (0, state_sync_1.safeSessionId)(sandboxKey)), sandboxImageUser);
|
|
756
812
|
return { readonly: sandboxMounts.readonly, writable: [`${dir}:${containerDriver_1.SANDBOX_WORKSPACE_DIR}`] };
|
|
757
813
|
};
|
|
758
814
|
/**
|
|
@@ -814,6 +870,7 @@ async function serveCommand(options) {
|
|
|
814
870
|
// must reach an operator who did not pass --verbose.
|
|
815
871
|
? new containerDriver_1.ContainerSandboxDriver({
|
|
816
872
|
exec: support_1.dockerExec,
|
|
873
|
+
socketOwner: sandboxImageUser,
|
|
817
874
|
logger: {
|
|
818
875
|
info: (m) => logger.info(m),
|
|
819
876
|
warn: (m) => console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${m}`),
|
|
@@ -872,7 +929,9 @@ async function serveCommand(options) {
|
|
|
872
929
|
}),
|
|
873
930
|
// Kill switch + rollback, checked before a sandbox can exist. Revoking the lease stops
|
|
874
931
|
// new work fleet-wide immediately; TTL bounds the sandboxes already running.
|
|
875
|
-
|
|
932
|
+
// sessionKey, not sandboxKey: deny rules can name a SESSION, and under user scope the
|
|
933
|
+
// sandbox key names the user instead — checking that would stop matching them.
|
|
934
|
+
admit: async ({ sessionKey }) => {
|
|
876
935
|
const currentLease = await leaseSource.get();
|
|
877
936
|
assertPackAdmitted(currentLease, sessionKey);
|
|
878
937
|
},
|
|
@@ -887,11 +946,11 @@ async function serveCommand(options) {
|
|
|
887
946
|
// packId here made every downstream user_id (LiteLLM, spend rows, evidence provenance)
|
|
888
947
|
// the pack. Every mint runs inside runWithCredential (create-time on first-turn
|
|
889
948
|
// acquire, then per-turn re-mints in RemoteEngine.stream), so the ambient credential is
|
|
890
|
-
// the turn's own. An unidentified turn gets a
|
|
891
|
-
// silently borrowing the pack's name —
|
|
892
|
-
// marker cannot collide across identified users.
|
|
893
|
-
mintModelToken: (sandboxId,
|
|
894
|
-
user: (0, credential_context_1.currentCredential)()?.userId?.trim() || `anon:${
|
|
949
|
+
// the turn's own. An unidentified turn gets a sandbox-scoped marker rather than
|
|
950
|
+
// silently borrowing the pack's name — the sandbox key is user-namespaced under either
|
|
951
|
+
// scope, so the marker cannot collide across identified users.
|
|
952
|
+
mintModelToken: (sandboxId, sandboxKey) => (0, support_1.mintSandboxInferenceWarrant)(warrantKey, {
|
|
953
|
+
user: (0, credential_context_1.currentCredential)()?.userId?.trim() || `anon:${sandboxKey}`,
|
|
895
954
|
pack: packId,
|
|
896
955
|
packVersion: activeVersion,
|
|
897
956
|
sandboxId,
|
|
@@ -990,6 +1049,51 @@ async function serveCommand(options) {
|
|
|
990
1049
|
logger.info('[Serve] lease signature is NOT required — set --require-signed-lease (or lease.requireSignedLease) once the control plane signs leases.');
|
|
991
1050
|
}
|
|
992
1051
|
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Unattended runs (PLAN/agent-identity.md §5). Two gates, BOTH operator-side, plus a third
|
|
1054
|
+
* that needs no code here: the identity must be mapped in the connector's role map or the
|
|
1055
|
+
* connector fails the call closed exactly as it does for an unmapped human.
|
|
1056
|
+
*
|
|
1057
|
+
* Nothing in the pack participates. Enabling or disabling this never requires a re-sign.
|
|
1058
|
+
*/
|
|
1059
|
+
const serviceIdentity = (0, serve_hardening_1.resolveServiceIdentity)();
|
|
1060
|
+
if (serviceIdentity.error) {
|
|
1061
|
+
// A malformed value must not degrade to "anonymous": that shape looks configured and runs
|
|
1062
|
+
// every scheduled job with no identity at all.
|
|
1063
|
+
console.error(`${utils_1.COLORS.red}Error:${utils_1.COLORS.reset} ${serviceIdentity.error}`);
|
|
1064
|
+
process.exit(1);
|
|
1065
|
+
}
|
|
1066
|
+
if (serviceIdentity.warning) {
|
|
1067
|
+
console.warn(`${utils_1.COLORS.yellow}Warning:${utils_1.COLORS.reset} ${serviceIdentity.warning}`);
|
|
1068
|
+
}
|
|
1069
|
+
const serviceTurnsManaged = Boolean(process.env.AGENTHIPPO_LEASE_URL);
|
|
1070
|
+
/** Resolved per turn, not at boot: revoking in the console must take effect on the next poll. */
|
|
1071
|
+
const serviceUserIfPermitted = async () => {
|
|
1072
|
+
if (!serviceIdentity.identity) {
|
|
1073
|
+
return undefined;
|
|
1074
|
+
}
|
|
1075
|
+
const gate = (0, lease_2.resolveAllowServiceTurns)({
|
|
1076
|
+
lease: (await leaseSource.get())?.allowServiceTurns,
|
|
1077
|
+
managed: serviceTurnsManaged,
|
|
1078
|
+
});
|
|
1079
|
+
if (!gate.allowed) {
|
|
1080
|
+
// Named gate, so the operator reads which knob to turn rather than "denied".
|
|
1081
|
+
auditEvent('service-turn', 'service_turn_denied', {
|
|
1082
|
+
identity: (0, identity_1.hashIdentity)(serviceIdentity.identity),
|
|
1083
|
+
gate: gate.gate,
|
|
1084
|
+
detail: (0, lease_2.describeServiceTurnGate)(gate.gate),
|
|
1085
|
+
});
|
|
1086
|
+
return undefined;
|
|
1087
|
+
}
|
|
1088
|
+
return serviceIdentity.identity;
|
|
1089
|
+
};
|
|
1090
|
+
if (serviceIdentity.identity) {
|
|
1091
|
+
const boot = (0, lease_2.resolveAllowServiceTurns)({ lease: undefined, managed: serviceTurnsManaged });
|
|
1092
|
+
logger.info(`[Serve] unattended runs: identity configured (${serve_hardening_1.SERVICE_USER_ENV}); `
|
|
1093
|
+
+ (serviceTurnsManaged
|
|
1094
|
+
? 'the control plane decides per turn via lease.allowServiceTurns — absent means deny'
|
|
1095
|
+
: (0, lease_2.describeServiceTurnGate)(boot.gate)));
|
|
1096
|
+
}
|
|
993
1097
|
if (sandboxRunner) {
|
|
994
1098
|
logger.info(`[Serve] sandbox mode: ${options.sandbox} (tier ${options.sandbox === 'container' ? 2 : 0}) — engines run in per-session sandboxes`);
|
|
995
1099
|
// A sandbox's ONLY model credential is the per-sandbox warrant, which is meaningful only to an
|
|
@@ -1111,11 +1215,11 @@ async function serveCommand(options) {
|
|
|
1111
1215
|
}
|
|
1112
1216
|
// User-namespaced session key (secure-obo-design §3.4): kills cross-user session
|
|
1113
1217
|
// attachment via a guessed session_id. Fails closed on pinned-identity deployments.
|
|
1114
|
-
const { sessionKey, rejected } = (0, serve_hardening_1.resolveServeSessionKey)(body.session_id, userId);
|
|
1218
|
+
const { sessionKey, sandboxKey, rejected } = (0, serve_hardening_1.resolveServeSessionKey)(body.session_id, userId, sandboxScope);
|
|
1115
1219
|
if (rejected) {
|
|
1116
1220
|
sessionEventWriter.append('unverified', (0, base_1.generateRequestId)(), 'warrant_denied', { reason: rejected });
|
|
1117
1221
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1118
|
-
res.end(JSON.stringify({ error: '
|
|
1222
|
+
res.end(JSON.stringify({ error: 'verified user identity required' }));
|
|
1119
1223
|
return;
|
|
1120
1224
|
}
|
|
1121
1225
|
// Acquire before turn-limiter so a capacity reject does not burn a turn slot.
|
|
@@ -1143,7 +1247,7 @@ async function serveCommand(options) {
|
|
|
1143
1247
|
if (userId?.trim()) {
|
|
1144
1248
|
turnReg = await registerServeTurn({ userId: userId.trim(), sessionKey, inboundToken: dataToken, edgeNonce: edgeTurnNonce(req.headers) });
|
|
1145
1249
|
}
|
|
1146
|
-
const result = await (0, credential_context_1.runWithCredential)({ userId, dataToken }, () => handleChat(runner, body, sessionKey, defaultEngine, defaultModel, options.workspace, permissions, userId, turnReg?.env), { onDenied: logWarrantDenied(sessionKey) });
|
|
1250
|
+
const result = await (0, credential_context_1.runWithCredential)({ userId, dataToken }, () => handleChat(runner, body, sessionKey, sandboxKey, defaultEngine, defaultModel, options.workspace, permissions, userId, turnReg?.env), { onDenied: logWarrantDenied(sessionKey) });
|
|
1147
1251
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1148
1252
|
res.end(JSON.stringify(result));
|
|
1149
1253
|
statePusher?.schedule(sessionKey); // per-session delta push, off the response path
|
|
@@ -1168,11 +1272,11 @@ async function serveCommand(options) {
|
|
|
1168
1272
|
return;
|
|
1169
1273
|
}
|
|
1170
1274
|
// Same user-namespaced session key + fail-closed rule as /v1/chat above.
|
|
1171
|
-
const { sessionKey, rejected } = (0, serve_hardening_1.resolveServeSessionKey)(body.session_id, userId);
|
|
1275
|
+
const { sessionKey, sandboxKey, rejected } = (0, serve_hardening_1.resolveServeSessionKey)(body.session_id, userId, sandboxScope);
|
|
1172
1276
|
if (rejected) {
|
|
1173
1277
|
sessionEventWriter.append('unverified', (0, base_1.generateRequestId)(), 'warrant_denied', { reason: rejected });
|
|
1174
1278
|
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
1175
|
-
res.end(JSON.stringify({ error: '
|
|
1279
|
+
res.end(JSON.stringify({ error: 'verified user identity required' }));
|
|
1176
1280
|
return;
|
|
1177
1281
|
}
|
|
1178
1282
|
// Acquire BEFORE writeHead(200, SSE) so a reject still gets a clean 429.
|
|
@@ -1208,7 +1312,7 @@ async function serveCommand(options) {
|
|
|
1208
1312
|
'Cache-Control': 'no-cache',
|
|
1209
1313
|
'Connection': 'keep-alive',
|
|
1210
1314
|
});
|
|
1211
|
-
await handleStreamingChat(runner, body, sessionKey, defaultEngine, defaultModel, options.workspace, permissions, res, userId, turnReg?.env);
|
|
1315
|
+
await handleStreamingChat(runner, body, sessionKey, sandboxKey, defaultEngine, defaultModel, options.workspace, permissions, res, userId, turnReg?.env);
|
|
1212
1316
|
}, { onDenied: logWarrantDenied(sessionKey) });
|
|
1213
1317
|
statePusher?.schedule(sessionKey); // per-session delta push, off the response path
|
|
1214
1318
|
}
|
|
@@ -1330,8 +1434,10 @@ async function serveCommand(options) {
|
|
|
1330
1434
|
turnLimiter,
|
|
1331
1435
|
warnGateReject,
|
|
1332
1436
|
prepareSession,
|
|
1437
|
+
sandboxScope,
|
|
1333
1438
|
onTurnComplete: (sessionId) => statePusher?.schedule(sessionId),
|
|
1334
1439
|
logWarrantDenied,
|
|
1440
|
+
serviceUserIfPermitted,
|
|
1335
1441
|
});
|
|
1336
1442
|
}
|
|
1337
1443
|
catch (err) {
|
|
@@ -1419,7 +1525,7 @@ async function parseBody(req) {
|
|
|
1419
1525
|
throw new Error('Invalid JSON');
|
|
1420
1526
|
}
|
|
1421
1527
|
}
|
|
1422
|
-
async function handleChat(runner, body, sessionKey, defaultEngine, defaultModel, workspace, permissions, userId, turnEnv) {
|
|
1528
|
+
async function handleChat(runner, body, sessionKey, sandboxKey, defaultEngine, defaultModel, workspace, permissions, userId, turnEnv) {
|
|
1423
1529
|
const result = await runner.run({
|
|
1424
1530
|
engineId: resolveRequestedEngineId(body.engine, defaultEngine),
|
|
1425
1531
|
modelId: body.model || defaultModel,
|
|
@@ -1428,6 +1534,9 @@ async function handleChat(runner, body, sessionKey, defaultEngine, defaultModel,
|
|
|
1428
1534
|
// The user-namespaced key (never raw body.session_id) so engine conversation
|
|
1429
1535
|
// state and per-session artifacts are per-user — see resolveServeSessionKey.
|
|
1430
1536
|
sessionId: sessionKey,
|
|
1537
|
+
// Which sandbox runs it. Equal to sessionKey unless --sandbox-scope user; ignored
|
|
1538
|
+
// entirely by the in-process runner.
|
|
1539
|
+
sandboxKey,
|
|
1431
1540
|
analyticsUserId: userId,
|
|
1432
1541
|
permissions,
|
|
1433
1542
|
turnEnv,
|
|
@@ -1462,21 +1571,70 @@ async function handleChat(runner, body, sessionKey, defaultEngine, defaultModel,
|
|
|
1462
1571
|
* reply handles under it (bridge.ts upsertGatewayOutboundHandle), so the outbound-routing
|
|
1463
1572
|
* file must keep resolving from the un-namespaced key or reply-to-sender breaks.
|
|
1464
1573
|
*/
|
|
1465
|
-
function resolveGatewaySessionKey(opts, userId
|
|
1574
|
+
function resolveGatewaySessionKey(opts, userId, scope = 'session',
|
|
1575
|
+
/**
|
|
1576
|
+
* This turn runs as the deployment's SERVICE identity — no human behind it
|
|
1577
|
+
* (PLAN/agent-identity.md §5.4). Passed explicitly rather than sniffed from a prefix on
|
|
1578
|
+
* `userId`: deriving behaviour from the shape of an identity string is how the `"false"` class
|
|
1579
|
+
* of bug happens, and §5.2 deliberately allows identities with no recognisable prefix at all.
|
|
1580
|
+
*/
|
|
1581
|
+
isServiceIdentity = false) {
|
|
1582
|
+
if (isServiceIdentity && userId) {
|
|
1583
|
+
// Two independent reasons this branch cannot reuse the durable human shape.
|
|
1584
|
+
//
|
|
1585
|
+
// sessionKey FRESH: TurnLimiter is a monotonic per-session counter with no window, no
|
|
1586
|
+
// reset and no decay (serve-hardening.ts). A recurring job on a stable key therefore burns
|
|
1587
|
+
// a LIFETIME budget one run per night and, on night maxTurnsPerSession + 1, is rejected
|
|
1588
|
+
// with `turn_limit` forever — recovering only on a serve restart. A nightly job that dies
|
|
1589
|
+
// after N nights, where N is a cap nobody associates with cron.
|
|
1590
|
+
//
|
|
1591
|
+
// sandboxKey STABLE: under the default --sandbox-scope session it would otherwise track
|
|
1592
|
+
// sessionKey, so "fresh key per run" would silently mean a NEW CONTAINER every night —
|
|
1593
|
+
// cold start, a new governor entry per run, and maxSessionSeconds that never accumulates.
|
|
1594
|
+
// Holding it stable gives the bot one warm sandbox and, more importantly, ONE handle for
|
|
1595
|
+
// caps and the kill switch.
|
|
1596
|
+
//
|
|
1597
|
+
// Net shape: fresh conversation, warm sandbox.
|
|
1598
|
+
const stable = `gateway-${userId}`;
|
|
1599
|
+
return {
|
|
1600
|
+
sessionKey: `${stable}--${(0, node_crypto_1.randomUUID)()}`,
|
|
1601
|
+
sandboxKey: stable,
|
|
1602
|
+
// Replies must still route: the egress broker resolves this run's destinations from the
|
|
1603
|
+
// raw channel key, so a cron job that messages Slack keeps working.
|
|
1604
|
+
outboundKey: opts.sessionId?.trim() || opts.gatewaySessionKey?.trim() || stable,
|
|
1605
|
+
// No prior state to restore and no future turn to restore it for. A batch job that
|
|
1606
|
+
// silently accumulates context also silently accumulates cost; continuity across runs
|
|
1607
|
+
// stays available by passing a stable session_id, which takes the durable branch below.
|
|
1608
|
+
durable: false,
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
// The SAME derivation resolveServeSessionKey uses. If the two disagreed, one person reaching
|
|
1612
|
+
// the same serve over HTTP and over a channel would get two sandboxes under user scope —
|
|
1613
|
+
// neither the old behaviour nor the new one.
|
|
1614
|
+
const userSandboxKey = userId ? `u${(0, serve_hardening_1.userNamespaceHash)(userId)}` : undefined;
|
|
1466
1615
|
const explicit = opts.sessionId?.trim() || opts.gatewaySessionKey?.trim();
|
|
1467
1616
|
if (explicit) {
|
|
1468
1617
|
const sessionKey = userId ? `u${(0, serve_hardening_1.userNamespaceHash)(userId)}--${explicit}` : explicit;
|
|
1469
|
-
|
|
1618
|
+
const sandboxKey = scope === 'user' && userSandboxKey ? userSandboxKey : sessionKey;
|
|
1619
|
+
return { sessionKey, sandboxKey, outboundKey: explicit, durable: true };
|
|
1470
1620
|
}
|
|
1471
1621
|
if (userId) {
|
|
1472
1622
|
// Already keyed on the sender's own identity — no cross-user attachment surface.
|
|
1473
|
-
|
|
1623
|
+
const sessionKey = `gateway-${userId}`;
|
|
1624
|
+
return {
|
|
1625
|
+
sessionKey,
|
|
1626
|
+
sandboxKey: scope === 'user' && userSandboxKey ? userSandboxKey : sessionKey,
|
|
1627
|
+
outboundKey: sessionKey,
|
|
1628
|
+
durable: true,
|
|
1629
|
+
};
|
|
1474
1630
|
}
|
|
1475
1631
|
// randomUUID (122 bits), not generateRequestId (~32 bits) — this is the sole thing
|
|
1476
1632
|
// standing between two unrelated anonymous senders and a real collision, so it needs to
|
|
1477
1633
|
// be architecturally collision-free, not just statistically unlikely.
|
|
1478
1634
|
const anon = `gateway-anon-${(0, node_crypto_1.randomUUID)()}`;
|
|
1479
|
-
|
|
1635
|
+
// Its own sandbox, under either scope: with no identity there is no user to group by, and
|
|
1636
|
+
// grouping unidentified senders would be inventing one.
|
|
1637
|
+
return { sessionKey: anon, sandboxKey: anon, outboundKey: anon, durable: false };
|
|
1480
1638
|
}
|
|
1481
1639
|
/**
|
|
1482
1640
|
* Gateway-channel turn admission: rate limit → turn gate (capacity) → per-session turn cap,
|
|
@@ -1539,79 +1697,112 @@ async function startServeGatewayBridge(params) {
|
|
|
1539
1697
|
workspace: params.workspace,
|
|
1540
1698
|
engineId: params.engine,
|
|
1541
1699
|
syncOpenClawConfigAgent: true,
|
|
1542
|
-
messageHandler:
|
|
1543
|
-
const requestedEngine = opts.engineId?.trim().toLowerCase();
|
|
1544
|
-
const effectiveEngine = requestedEngine === 'custom'
|
|
1545
|
-
? params.engine
|
|
1546
|
-
: (requestedEngine || params.engine);
|
|
1547
|
-
// #4: the channel sender becomes the per-user user_id (analytics + per-user data scoping).
|
|
1548
|
-
const userId = (0, serve_hardening_1.peerToUserId)(opts.peerInfo);
|
|
1549
|
-
const { sessionKey, outboundKey, durable } = resolveGatewaySessionKey(opts, userId);
|
|
1550
|
-
return runGatewayTurn({
|
|
1551
|
-
rateLimiter: params.rateLimiter,
|
|
1552
|
-
turnLimiter: params.turnLimiter,
|
|
1553
|
-
turnGate: params.turnGate,
|
|
1554
|
-
warnGateReject: params.warnGateReject,
|
|
1555
|
-
// Same identity-keyed cap the HTTP endpoints enforce — a channel sender is already
|
|
1556
|
-
// trusted enough here to gate the OBO credential scope below, so it's trusted enough
|
|
1557
|
-
// to rate-limit on directly. Unidentified senders share one 'gateway' bucket rather
|
|
1558
|
-
// than being exempt from throttling entirely.
|
|
1559
|
-
rlKey: userId ?? 'gateway',
|
|
1560
|
-
sessionKey,
|
|
1561
|
-
trackSession: durable,
|
|
1562
|
-
emitter: opts.emitter,
|
|
1563
|
-
run: async () => {
|
|
1564
|
-
// A one-off anonymous sessionKey can never have prior state to restore, and
|
|
1565
|
-
// nothing will ever look it up again to push state for — skip both S3 round-trips.
|
|
1566
|
-
if (durable) {
|
|
1567
|
-
await params.prepareSession(sessionKey);
|
|
1568
|
-
}
|
|
1569
|
-
// Channel-originated turns carry no inbound platform token: with the
|
|
1570
|
-
// passthrough provider they fail closed at the gateway (an unlinked chat
|
|
1571
|
-
// peer gets NO data credential — secure-obo-design §7.5 bind-before-
|
|
1572
|
-
// credential); the identity provider maps the peer id server-side instead.
|
|
1573
|
-
// egressStoreKey is the RAW gateway key (outboundKey) the bridge records reply
|
|
1574
|
-
// handles under — it lets the egress broker resolve this session's bound
|
|
1575
|
-
// destinations server-side (§7.6). userId gates BOTH capabilities: an
|
|
1576
|
-
// unidentified peer gets neither a nonce nor an egress route.
|
|
1577
|
-
const turnReg = userId?.trim()
|
|
1578
|
-
? await params.registerTurn({ userId: userId.trim(), sessionKey, egressStoreKey: outboundKey })
|
|
1579
|
-
: null;
|
|
1580
|
-
try {
|
|
1581
|
-
const result = await (0, credential_context_1.runWithCredential)({ userId }, () => params.runner.run({
|
|
1582
|
-
engineId: effectiveEngine,
|
|
1583
|
-
modelId: params.defaultModel,
|
|
1584
|
-
message,
|
|
1585
|
-
workspaceRoot: params.workspace,
|
|
1586
|
-
// Use the resolved sessionKey (not the raw opts fields) so the engine's own
|
|
1587
|
-
// session state and the S3 restore/push above key off the same, user-namespaced
|
|
1588
|
-
// identity. The outbound-routing file (buildSessionEnvPatch) keeps the raw
|
|
1589
|
-
// gateway key — the bridge records reply handles under it, so namespacing it
|
|
1590
|
-
// here would break reply-to-sender. See resolveGatewaySessionKey.
|
|
1591
|
-
sessionId: sessionKey,
|
|
1592
|
-
gatewaySessionKey: outboundKey,
|
|
1593
|
-
analyticsUserId: userId,
|
|
1594
|
-
emitter: opts.emitter,
|
|
1595
|
-
permissions: params.permissions,
|
|
1596
|
-
turnEnv: turnReg?.env,
|
|
1597
|
-
}), { onDenied: params.logWarrantDenied(sessionKey) });
|
|
1598
|
-
if (durable) {
|
|
1599
|
-
params.onTurnComplete?.(sessionKey);
|
|
1600
|
-
}
|
|
1601
|
-
return { success: result.success, error: result.error };
|
|
1602
|
-
}
|
|
1603
|
-
finally {
|
|
1604
|
-
await turnReg?.release();
|
|
1605
|
-
}
|
|
1606
|
-
},
|
|
1607
|
-
});
|
|
1608
|
-
},
|
|
1700
|
+
messageHandler: (message, opts) => runBridgeTurn(params, message, opts),
|
|
1609
1701
|
});
|
|
1610
1702
|
console.log(`${utils_1.COLORS.green}✓${utils_1.COLORS.reset} Gateway bridge: registered "${params.gatewayAgentId}" at ${gatewayUrl}`);
|
|
1611
1703
|
console.log(`${utils_1.COLORS.dim} Gateway outbound routes: ${params.workspace}/.agent-hippo/sessions/<sessionKey>/gateway-outbound.json${utils_1.COLORS.reset}`);
|
|
1612
1704
|
console.log(`${utils_1.COLORS.dim} openclaw agent --agent ${params.gatewayAgentId} --message "…"${utils_1.COLORS.reset}`);
|
|
1613
1705
|
return bridge;
|
|
1614
1706
|
}
|
|
1707
|
+
/**
|
|
1708
|
+
* One gateway-bridge turn, extracted from the message handler so the shipped path is directly
|
|
1709
|
+
* testable.
|
|
1710
|
+
*
|
|
1711
|
+
* It was previously an inline closure inside `bridge.registerAgent`, reachable only through a live
|
|
1712
|
+
* OpenClaw websocket — so the composition that actually delivers unattended runs (five identity
|
|
1713
|
+
* sites, four session-key values, the limiter, the registry) had no test that ran it. Transcribing
|
|
1714
|
+
* it into a test would have been a second implementation, free to drift from the one that ships.
|
|
1715
|
+
*/
|
|
1716
|
+
async function runBridgeTurn(params, message, opts) {
|
|
1717
|
+
const requestedEngine = opts.engineId?.trim().toLowerCase();
|
|
1718
|
+
const effectiveEngine = requestedEngine === 'custom'
|
|
1719
|
+
? params.engine
|
|
1720
|
+
: (requestedEngine || params.engine);
|
|
1721
|
+
// #4: the channel sender becomes the per-user user_id (analytics + per-user data scoping).
|
|
1722
|
+
const peerUser = (0, serve_hardening_1.peerToUserId)(opts.peerInfo);
|
|
1723
|
+
// No chat peer ⇒ this is an unattended run (cron, or a channel event with no sender).
|
|
1724
|
+
// It may act as the deployment's own service identity if BOTH operator-side gates
|
|
1725
|
+
// allow it (PLAN/agent-identity.md §5.6). Never on the anonymous HTTP lane — that is
|
|
1726
|
+
// externally reachable, and an unauthenticated caller must not be promotable to a
|
|
1727
|
+
// standing identity; this handler is the gateway-bridge lane only.
|
|
1728
|
+
const svcUser = peerUser ? undefined : await params.serviceUserIfPermitted?.();
|
|
1729
|
+
const effectiveUser = peerUser ?? svcUser;
|
|
1730
|
+
// effectiveUser flows to FIVE places, not two. Missing any of them degrades the design
|
|
1731
|
+
// silently rather than loudly — see each site below.
|
|
1732
|
+
const { sessionKey, sandboxKey, outboundKey, durable } = resolveGatewaySessionKey(opts, effectiveUser, params.sandboxScope, Boolean(svcUser));
|
|
1733
|
+
return runGatewayTurn({
|
|
1734
|
+
rateLimiter: params.rateLimiter,
|
|
1735
|
+
turnLimiter: params.turnLimiter,
|
|
1736
|
+
turnGate: params.turnGate,
|
|
1737
|
+
warnGateReject: params.warnGateReject,
|
|
1738
|
+
// (1) Same identity-keyed cap the HTTP endpoints enforce — a channel sender is already
|
|
1739
|
+
// trusted enough here to gate the OBO credential scope below, so it's trusted enough
|
|
1740
|
+
// to rate-limit on directly. Unidentified senders share one 'gateway' bucket rather
|
|
1741
|
+
// than being exempt from throttling entirely. A service turn gets its OWN bucket:
|
|
1742
|
+
// leaving it in the shared pool would let one bot's schedule throttle every
|
|
1743
|
+
// anonymous sender, and vice versa.
|
|
1744
|
+
rlKey: effectiveUser ?? 'gateway',
|
|
1745
|
+
sessionKey,
|
|
1746
|
+
trackSession: durable,
|
|
1747
|
+
emitter: opts.emitter,
|
|
1748
|
+
run: async () => {
|
|
1749
|
+
// A one-off anonymous sessionKey can never have prior state to restore, and
|
|
1750
|
+
// nothing will ever look it up again to push state for — skip both S3 round-trips.
|
|
1751
|
+
if (durable) {
|
|
1752
|
+
await params.prepareSession(sessionKey);
|
|
1753
|
+
}
|
|
1754
|
+
// Channel-originated turns carry no inbound platform token: with the
|
|
1755
|
+
// passthrough provider they fail closed at the gateway (an unlinked chat
|
|
1756
|
+
// peer gets NO data credential — secure-obo-design §7.5 bind-before-
|
|
1757
|
+
// credential); the identity provider maps the peer id server-side instead.
|
|
1758
|
+
// egressStoreKey is the RAW gateway key (outboundKey) the bridge records reply
|
|
1759
|
+
// handles under — it lets the egress broker resolve this session's bound
|
|
1760
|
+
// destinations server-side (§7.6). userId gates BOTH capabilities: an
|
|
1761
|
+
// unidentified peer gets neither a nonce nor an egress route.
|
|
1762
|
+
// (2) Data authority.
|
|
1763
|
+
const turnReg = effectiveUser?.trim()
|
|
1764
|
+
? await params.registerTurn({ userId: effectiveUser.trim(), sessionKey, egressStoreKey: outboundKey })
|
|
1765
|
+
: null;
|
|
1766
|
+
try {
|
|
1767
|
+
// (3) Warrant + spend. The easy site to miss and the expensive one: the
|
|
1768
|
+
// warrant's `user` claim reads `currentCredential()?.userId ?? anon:<sandboxKey>`,
|
|
1769
|
+
// so registering without ALSO scoping here gives the bot data access under its
|
|
1770
|
+
// service identity while its SPEND attributes to `anon:` — outside the per-user
|
|
1771
|
+
// meter and unattributable in evidence.
|
|
1772
|
+
const result = await (0, credential_context_1.runWithCredential)({ userId: effectiveUser }, () => params.runner.run({
|
|
1773
|
+
engineId: effectiveEngine,
|
|
1774
|
+
modelId: params.defaultModel,
|
|
1775
|
+
message,
|
|
1776
|
+
workspaceRoot: params.workspace,
|
|
1777
|
+
// Use the resolved sessionKey (not the raw opts fields) so the engine's own
|
|
1778
|
+
// session state and the S3 restore/push above key off the same, user-namespaced
|
|
1779
|
+
// identity. The outbound-routing file (buildSessionEnvPatch) keeps the raw
|
|
1780
|
+
// gateway key — the bridge records reply handles under it, so namespacing it
|
|
1781
|
+
// here would break reply-to-sender. See resolveGatewaySessionKey.
|
|
1782
|
+
sessionId: sessionKey,
|
|
1783
|
+
// (4) Which sandbox runs it — stable across runs for a service
|
|
1784
|
+
// identity, so a nightly job reuses one warm container.
|
|
1785
|
+
sandboxKey,
|
|
1786
|
+
gatewaySessionKey: outboundKey,
|
|
1787
|
+
// (5) Attribution. Left unset, Spotlight rows would have no user
|
|
1788
|
+
// while the warrant claims the service identity — two attribution
|
|
1789
|
+
// channels disagreeing, which is worse than one being wrong.
|
|
1790
|
+
analyticsUserId: effectiveUser,
|
|
1791
|
+
emitter: opts.emitter,
|
|
1792
|
+
permissions: params.permissions,
|
|
1793
|
+
turnEnv: turnReg?.env,
|
|
1794
|
+
}), { onDenied: params.logWarrantDenied(sessionKey) });
|
|
1795
|
+
if (durable) {
|
|
1796
|
+
params.onTurnComplete?.(sessionKey);
|
|
1797
|
+
}
|
|
1798
|
+
return { success: result.success, error: result.error };
|
|
1799
|
+
}
|
|
1800
|
+
finally {
|
|
1801
|
+
await turnReg?.release();
|
|
1802
|
+
}
|
|
1803
|
+
},
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1615
1806
|
/**
|
|
1616
1807
|
* Connect to the gateway and set channel bindings for this agent.
|
|
1617
1808
|
* Called when --bind is specified (e.g., --bind "telegram,whatsapp" or --bind "*").
|
|
@@ -1667,7 +1858,7 @@ async function bindToGatewayChannels(agentId, bindSpec, token) {
|
|
|
1667
1858
|
client.close();
|
|
1668
1859
|
}
|
|
1669
1860
|
}
|
|
1670
|
-
async function handleStreamingChat(runner, body, sessionKey, defaultEngine, defaultModel, workspace, permissions, res, userId, turnEnv) {
|
|
1861
|
+
async function handleStreamingChat(runner, body, sessionKey, sandboxKey, defaultEngine, defaultModel, workspace, permissions, res, userId, turnEnv) {
|
|
1671
1862
|
let streamEnded = false;
|
|
1672
1863
|
const sendEvent = (type, data) => {
|
|
1673
1864
|
if (streamEnded || res.writableEnded) {
|
|
@@ -1723,6 +1914,7 @@ async function handleStreamingChat(runner, body, sessionKey, defaultEngine, defa
|
|
|
1723
1914
|
workspaceRoot: workspace,
|
|
1724
1915
|
// The user-namespaced key (never raw body.session_id) — see resolveServeSessionKey.
|
|
1725
1916
|
sessionId: sessionKey,
|
|
1917
|
+
sandboxKey,
|
|
1726
1918
|
analyticsUserId: userId,
|
|
1727
1919
|
emitter,
|
|
1728
1920
|
permissions,
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ON_DENIED_TIMEOUT_MS = exports.WARRANT_DENIED_REASON = void 0;
|
|
7
7
|
exports.runWithCredential = runWithCredential;
|
|
8
8
|
exports.currentCredential = currentCredential;
|
|
9
|
+
exports.resolveEngineHomeUserKey = resolveEngineHomeUserKey;
|
|
9
10
|
exports.requireUserId = requireUserId;
|
|
10
11
|
/**
|
|
11
12
|
* Request-scoped credential context — the "Design B" seam from serve-v3 §11.1.
|
|
@@ -23,6 +24,7 @@ exports.requireUserId = requireUserId;
|
|
|
23
24
|
* (secure-obo-design §2/§3 — the agent gets a capability, not the credential).
|
|
24
25
|
*/
|
|
25
26
|
const async_hooks_1 = require("async_hooks");
|
|
27
|
+
const paths_1 = require("../agentpack/paths");
|
|
26
28
|
/** Stable, queryable reason code for the one case runWithCredential rejects. */
|
|
27
29
|
exports.WARRANT_DENIED_REASON = 'token_without_verified_user';
|
|
28
30
|
/** Default bound on `onDenied` so a hung audit write cannot stall admission forever. */
|
|
@@ -78,6 +80,21 @@ async function runWithCredential(cred, fn, options) {
|
|
|
78
80
|
function currentCredential() {
|
|
79
81
|
return storage.getStore();
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The per-user engine-home segment for this turn, from whichever source actually has it.
|
|
85
|
+
*
|
|
86
|
+
* Two processes need the same answer and only one of them can read the credential. In serve the
|
|
87
|
+
* frame is authoritative. In a sandbox child there is no frame — that is the design, the
|
|
88
|
+
* credential never crosses — so the key arrives pre-computed on the run frame and is used as-is.
|
|
89
|
+
* The frame wins where both exist, so a child cannot widen its own scope by sending a key.
|
|
90
|
+
*
|
|
91
|
+
* Every engine must go through this rather than reading the credential directly: doing the latter
|
|
92
|
+
* is what silently unscoped every sandboxed turn, and the failure was invisible because the
|
|
93
|
+
* unscoped path is a perfectly good directory that simply holds everyone's data.
|
|
94
|
+
*/
|
|
95
|
+
function resolveEngineHomeUserKey(fromRunFrame) {
|
|
96
|
+
return (0, paths_1.engineHomeUserKey)(storage.getStore()?.userId) ?? fromRunFrame?.trim() ?? undefined;
|
|
97
|
+
}
|
|
81
98
|
/**
|
|
82
99
|
* Fail-closed accessor for the acting user id. Throws when there is no
|
|
83
100
|
* authenticated user in scope, so a tool can never silently run unscoped.
|
|
@@ -36,7 +36,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
36
36
|
};
|
|
37
37
|
})();
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.DataPlane = exports.resolveEdgeDataPlane = exports.EdgeDataPlane = exports.resolveRemoteDataPlane = exports.RemoteDataPlane = exports.startDataPlaneSidecar = exports.EGRESS_URL_ENV = exports.startEgressBroker = exports.TURN_NONCE_HEADER = exports.TURN_NONCE_ENV = exports.DATA_MCP_URL_ENV = exports.startDataGateway = exports.TURN_META_KEY = exports.ConnectorHost = exports.identityProvider = exports.passthroughProvider = exports.resolveProvider = exports.CredentialBroker = exports.DEFAULT_TURN_DEADLINE_MS = exports.hashNonce = exports.TurnRegistry = void 0;
|
|
39
|
+
exports.DataPlane = exports.resolveEdgeDataPlane = exports.EdgeDataPlane = exports.resolveRemoteDataPlane = exports.RemoteDataPlane = exports.ASSERTION_HEADER = exports.startDataPlaneSidecar = exports.EGRESS_URL_ENV = exports.startEgressBroker = exports.TURN_NONCE_HEADER = exports.TURN_NONCE_ENV = exports.DATA_MCP_URL_ENV = exports.startDataGateway = exports.TURN_META_KEY = exports.ConnectorHost = exports.identityProvider = exports.passthroughProvider = exports.resolveProvider = exports.CredentialBroker = exports.DEFAULT_TURN_DEADLINE_MS = exports.hashNonce = exports.TurnRegistry = void 0;
|
|
40
40
|
exports.resolveDataPlaneConfig = resolveDataPlaneConfig;
|
|
41
41
|
/**
|
|
42
42
|
* DataPlane — orchestration of the credential broker + data-plane gateway
|
|
@@ -75,6 +75,7 @@ Object.defineProperty(exports, "startEgressBroker", { enumerable: true, get: fun
|
|
|
75
75
|
Object.defineProperty(exports, "EGRESS_URL_ENV", { enumerable: true, get: function () { return egress_1.EGRESS_URL_ENV; } });
|
|
76
76
|
var sidecar_1 = require("./sidecar");
|
|
77
77
|
Object.defineProperty(exports, "startDataPlaneSidecar", { enumerable: true, get: function () { return sidecar_1.startDataPlaneSidecar; } });
|
|
78
|
+
Object.defineProperty(exports, "ASSERTION_HEADER", { enumerable: true, get: function () { return sidecar_1.ASSERTION_HEADER; } });
|
|
78
79
|
var remote_1 = require("./remote");
|
|
79
80
|
Object.defineProperty(exports, "RemoteDataPlane", { enumerable: true, get: function () { return remote_1.RemoteDataPlane; } });
|
|
80
81
|
Object.defineProperty(exports, "resolveRemoteDataPlane", { enumerable: true, get: function () { return remote_1.resolveRemoteDataPlane; } });
|