@kafca/agentdock 0.1.61 → 0.1.62

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.
Files changed (127) hide show
  1. package/README.md +18 -0
  2. package/dist/renderer/assets/AutomationList-BgJgAdVO.js +16 -0
  3. package/dist/renderer/assets/{Badge-BLI4LHse.js → Badge-Bq2hPVcR.js} +1 -1
  4. package/dist/renderer/assets/{Card-DJeLDdL3.js → Card-BPJNSZce.js} +1 -1
  5. package/dist/renderer/assets/{Config-D1awc81A.js → Config-aSFqlI88.js} +2 -2
  6. package/dist/renderer/assets/Dashboard-D3ezFmb7.js +36 -0
  7. package/dist/renderer/assets/{EmptyState-CkI1RwPX.js → EmptyState-DK496gBq.js} +1 -1
  8. package/dist/renderer/assets/{HighlightedMarkdown-BUhDlhgW.js → HighlightedMarkdown-Ckre5jNv.js} +1 -1
  9. package/dist/renderer/assets/{Input-DHYrn-Ne.js → Input-C5C1d0ba.js} +1 -1
  10. package/dist/renderer/assets/{KnowledgeDetail-Bij-Xo90.js → KnowledgeDetail-N8Ey1ikc.js} +2 -2
  11. package/dist/renderer/assets/{KnowledgeHome-DmPZ2IlI.js → KnowledgeHome-9aF8nJEf.js} +2 -2
  12. package/dist/renderer/assets/{Logs-ISY99heA.js → Logs-sgAdwEeS.js} +2 -2
  13. package/dist/renderer/assets/{Modal-DAQfz0ot.js → Modal-BB10hTRc.js} +3 -3
  14. package/dist/renderer/assets/{Page-Dw43UYDT.js → Page-DfC319Ck.js} +1 -1
  15. package/dist/renderer/assets/{Select-DzmN5OGQ.js → Select-RHJBmNER.js} +1 -1
  16. package/dist/renderer/assets/{ThreadChat-B5CWqqMK.js → ThreadChat-BU1zCdr4.js} +3 -3
  17. package/dist/renderer/assets/{Workspace-C5g9LBj9.js → Workspace-BoZCz7S8.js} +1 -1
  18. package/dist/renderer/assets/{arrow-left-BLXh56Ip.js → arrow-left-D2V1eZSD.js} +1 -1
  19. package/dist/renderer/assets/{book-open-DhPGbc-7.js → book-open-DUcBn7Bb.js} +1 -1
  20. package/dist/renderer/assets/{channels-DfQrmuYO.js → channels-DMhDSlf2.js} +2 -2
  21. package/dist/renderer/assets/{chevron-down-CEFtpPDc.js → chevron-down-Ca2FQDB7.js} +1 -1
  22. package/dist/renderer/assets/en-Bnr4nMzg.js +1 -0
  23. package/dist/renderer/assets/es-B1GIFh3i.js +1 -0
  24. package/dist/renderer/assets/{index-BIKL3fTP.css → index-B6WcpkJF.css} +1 -1
  25. package/dist/renderer/assets/{index-CYhDUAR_.js → index-BlnvlekJ.js} +1 -1
  26. package/dist/renderer/assets/index-DRJ5wgw_.js +162 -0
  27. package/dist/renderer/assets/ja-CDWPWqEW.js +1 -0
  28. package/dist/renderer/assets/{knowledge-uHRQ80sR.js → knowledge-D1-k56eY.js} +1 -1
  29. package/dist/renderer/assets/{pencil-BD2Y31YC.js → pencil-BhWdv_86.js} +1 -1
  30. package/dist/renderer/assets/{plus-DDcldp_c.js → plus-BacLMlK3.js} +1 -1
  31. package/dist/renderer/assets/{save-CMW_cZI9.js → save-Bpoxbzau.js} +1 -1
  32. package/dist/renderer/assets/{search-BM-0CvJr.js → search-BVJcpdFX.js} +1 -1
  33. package/dist/renderer/assets/{shield-check-DaMB0FKG.js → shield-check-C2cw_Ip6.js} +1 -1
  34. package/dist/renderer/assets/{threads-DZgt9tWj.js → threads-DOsBQBg_.js} +1 -1
  35. package/dist/renderer/assets/{trash-2-DTXVbC5y.js → trash-2-Bs3rEm3g.js} +1 -1
  36. package/dist/renderer/assets/zh-MSddxMDX.js +1 -0
  37. package/dist/renderer/assets/zh-TW-C_LNWX0o.js +1 -0
  38. package/dist/renderer/index.html +2 -2
  39. package/dist-electron/electron/managed-skills/agent-browser/SKILL.md +41 -0
  40. package/dist-electron/electron/managed-skills/agent-browser/references/authentication.md +83 -0
  41. package/dist-electron/electron/managed-skills/agent-browser/references/commands.md +50 -0
  42. package/dist-electron/electron/managed-skills/agent-browser/references/profiling.md +20 -0
  43. package/dist-electron/electron/managed-skills/agent-browser/references/proxy-support.md +18 -0
  44. package/dist-electron/electron/managed-skills/agent-browser/references/session-management.md +17 -0
  45. package/dist-electron/electron/managed-skills/agent-browser/references/snapshot-refs.md +17 -0
  46. package/dist-electron/electron/managed-skills/agent-browser/references/video-recording.md +21 -0
  47. package/dist-electron/electron/managed-skills/agent-browser/templates/authenticated-session.sh +11 -0
  48. package/dist-electron/electron/managed-skills/agent-browser/templates/capture-workflow.sh +12 -0
  49. package/dist-electron/electron/managed-skills/agent-browser/templates/form-automation.sh +10 -0
  50. package/dist-electron/electron/managed-skills/condition-trigger/SKILL.md +21 -0
  51. package/dist-electron/electron/managed-skills/condition-trigger/scripts/register-condition-trigger.sh +106 -0
  52. package/dist-electron/electron/managed-skills/knowledge-base/SKILL.md +13 -0
  53. package/dist-electron/electron/managed-skills/knowledge-base/scripts/search-knowledge.sh +80 -0
  54. package/dist-electron/packages/contracts/src/automations.js +211 -0
  55. package/dist-electron/packages/contracts/src/index.js +1 -0
  56. package/dist-electron/packages/core-sdk/src/automations.js +87 -0
  57. package/dist-electron/packages/core-sdk/src/client.js +101 -0
  58. package/dist-electron/packages/core-sdk/src/index.js +2 -1
  59. package/dist-electron/services/local-ai-core/src/acp/store/automation-script-store.js +415 -0
  60. package/dist-electron/services/local-ai-core/src/acp/store/automation-store.js +829 -0
  61. package/dist-electron/services/local-ai-core/src/acp/store/local-core-acp-store.js +158 -0
  62. package/dist-electron/services/local-ai-core/src/acp/store/schema.js +81 -0
  63. package/dist-electron/services/local-ai-core/src/automation/automation-action-executor.js +144 -0
  64. package/dist-electron/services/local-ai-core/src/automation/automation-condition-engine.js +79 -0
  65. package/dist-electron/services/local-ai-core/src/automation/automation-conversation-executor.js +39 -73
  66. package/dist-electron/services/local-ai-core/src/automation/automation-monitor-service.js +695 -260
  67. package/dist-electron/services/local-ai-core/src/automation/automation-script-service.js +507 -0
  68. package/dist-electron/services/local-ai-core/src/automation/automation-service.js +1019 -0
  69. package/dist-electron/services/local-ai-core/src/automation/automation-trigger-engine.js +141 -0
  70. package/dist-electron/services/local-ai-core/src/automation/condition-evaluator.js +60 -17
  71. package/dist-electron/services/local-ai-core/src/automation/legacy-automation-mappers.js +286 -0
  72. package/dist-electron/services/local-ai-core/src/automation/scripts/anthropic-sandbox-runner.js +792 -0
  73. package/dist-electron/services/local-ai-core/src/automation/scripts/sandbox-runner.js +2 -0
  74. package/dist-electron/services/local-ai-core/src/automation/scripts/script-package.js +475 -0
  75. package/dist-electron/services/local-ai-core/src/automation/scripts/script-protocol-runner.js +266 -0
  76. package/dist-electron/services/local-ai-core/src/automation/scripts/secret-resolver.js +37 -0
  77. package/dist-electron/services/local-ai-core/src/channel/weixin/inbound-poller.js +1 -2
  78. package/dist-electron/services/local-ai-core/src/channel/weixin/transport.js +9 -8
  79. package/dist-electron/services/local-ai-core/src/cli/lac.js +229 -0
  80. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +21 -0
  81. package/dist-electron/services/local-ai-core/src/runtime/deployment-diagnostics.js +76 -0
  82. package/dist-electron/services/local-ai-core/src/runtime/handlers/automations-handler.js +322 -0
  83. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +20 -0
  84. package/dist-electron/services/local-ai-core/src/runtime/managed-skill-catalog.js +37 -0
  85. package/dist-electron/services/local-ai-core/src/runtime/server-helpers.js +14 -4
  86. package/dist-electron/services/local-ai-core/src/runtime/server-routes.js +75 -0
  87. package/dist-electron/services/local-ai-core/src/runtime/server.js +21 -0
  88. package/dist-electron/services/local-ai-core/src/runtime/standalone.js +1 -0
  89. package/dist-electron/services/local-ai-core/src/scheduler/cron.js +151 -39
  90. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-conversation-executor.js +2 -4
  91. package/dist-electron/services/local-ai-core/src/scheduler/scheduled-job-application-service.js +64 -12
  92. package/dist-electron/services/local-ai-core/src/scheduler/scheduler-service.js +71 -4
  93. package/dist-electron/services/local-ai-core/src/scheduler/thread-resolution.js +10 -0
  94. package/dist-electron/services/local-ai-core/src/thread/agent-message-policy.js +16 -1
  95. package/dist-electron/src/pages/Automation/automation-page-model.js +76 -0
  96. package/dist-electron/tests/contracts/agent-message-policy.test.js +32 -0
  97. package/dist-electron/tests/contracts/architecture-docs.test.js +30 -0
  98. package/dist-electron/tests/contracts/automation-contracts.test.js +154 -0
  99. package/dist-electron/tests/contracts/automation-renderer-model.test.js +47 -0
  100. package/dist-electron/tests/electron/automation-action-executor.test.js +75 -0
  101. package/dist-electron/tests/electron/automation-compatibility.test.js +463 -0
  102. package/dist-electron/tests/electron/automation-deployment.test.js +66 -0
  103. package/dist-electron/tests/electron/automation-engine.test.js +264 -0
  104. package/dist-electron/tests/electron/automation-monitor.test.js +1333 -0
  105. package/dist-electron/tests/electron/automation-sandbox-runner.test.js +333 -0
  106. package/dist-electron/tests/electron/automation-script-approval.test.js +553 -0
  107. package/dist-electron/tests/electron/automation-script-package.test.js +499 -0
  108. package/dist-electron/tests/electron/automation-script-runner.test.js +120 -0
  109. package/dist-electron/tests/electron/automation-service.test.js +1078 -0
  110. package/dist-electron/tests/electron/automation-store.test.js +593 -0
  111. package/dist-electron/tests/electron/condition-trigger-skill.test.js +93 -0
  112. package/dist-electron/tests/electron/core-client.test.js +99 -0
  113. package/dist-electron/tests/electron/lac-cli.test.js +110 -0
  114. package/dist-electron/tests/electron/workspace-task-store.test.js +25 -0
  115. package/dist-electron/tests/integration/automation-routes.test.js +186 -0
  116. package/dist-electron/tests/integration/automation-sandbox-runtime.test.js +92 -0
  117. package/package.json +4 -3
  118. package/dist/renderer/assets/CronList-CupZoNwW.js +0 -1
  119. package/dist/renderer/assets/Dashboard-nLZnOtcs.js +0 -41
  120. package/dist/renderer/assets/MonitorList-BlX82rXF.js +0 -1
  121. package/dist/renderer/assets/en-CmAr_0e6.js +0 -1
  122. package/dist/renderer/assets/es-Cs9x52ca.js +0 -1
  123. package/dist/renderer/assets/index-Dd00oqgY.js +0 -167
  124. package/dist/renderer/assets/ja-DtvtBRTW.js +0 -1
  125. package/dist/renderer/assets/play-DBJ8wsSy.js +0 -6
  126. package/dist/renderer/assets/zh-CcJ9vBZj.js +0 -1
  127. package/dist/renderer/assets/zh-TW-elI72AC1.js +0 -1
@@ -0,0 +1,93 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_fs_1 = require("node:fs");
9
+ const node_path_1 = require("node:path");
10
+ const node_os_1 = require("node:os");
11
+ const node_child_process_1 = require("node:child_process");
12
+ const managed_skill_catalog_js_1 = require("../../services/local-ai-core/src/runtime/managed-skill-catalog.js");
13
+ const sourceSkillPath = (0, node_path_1.join)(process.cwd(), 'electron', 'managed-skills', 'condition-trigger', 'SKILL.md');
14
+ (0, node_test_1.default)('condition trigger skill requires the staged two-approval workflow and helper owns exact LAC requests', () => {
15
+ const content = (0, node_fs_1.readFileSync)(sourceSkillPath, 'utf8');
16
+ strict_1.default.match(content, /temporary source bundle/i);
17
+ strict_1.default.match(content, /create.*Automation Script.*record/is);
18
+ strict_1.default.match(content, /manifest\.json.*entrypoint.*fixtures.*tests/is);
19
+ strict_1.default.match(content, /stage/i);
20
+ strict_1.default.match(content, /stop.*test authorization/is);
21
+ strict_1.default.match(content, /apply.*approved.*decision.*before.*test/is);
22
+ strict_1.default.match(content, /sandbox test/i);
23
+ strict_1.default.match(content, /stop.*final.*approval/is);
24
+ strict_1.default.match(content, /apply.*approved.*decision.*before.*Automation/is);
25
+ strict_1.default.match(content, /create.*Automation/is);
26
+ strict_1.default.match(content, /Do not write.*managed script directory/i);
27
+ strict_1.default.doesNotMatch(content, /automations\/scripts\/[^\s]+\/(?:sha|hash)/i);
28
+ const helper = (0, node_fs_1.readFileSync)((0, node_path_1.join)(process.cwd(), 'electron', 'managed-skills', 'condition-trigger', 'scripts', 'register-condition-trigger.sh'), 'utf8');
29
+ strict_1.default.match(helper, /lac script stage --script/);
30
+ strict_1.default.match(helper, /lac script create --title/);
31
+ strict_1.default.match(helper, /lac script test-approval/);
32
+ strict_1.default.match(helper, /lac script test/);
33
+ strict_1.default.match(helper, /lac script enable-approval/);
34
+ strict_1.default.match(helper, /lac script approve/);
35
+ strict_1.default.match(helper, /lac automation add --script-version/);
36
+ strict_1.default.match(helper, /--source-file/);
37
+ strict_1.default.doesNotMatch(helper, /--source-json/);
38
+ strict_1.default.match(helper, /openSync.*O_NOFOLLOW.*fstatSync/s);
39
+ strict_1.default.match(helper, /MAX_FILES.*MAX_BYTES/s);
40
+ strict_1.default.match(helper, /O_NOFOLLOW.*fstatSync.*readSync/s);
41
+ });
42
+ (0, node_test_1.default)('managed skill catalog loads exact source and packaged condition-trigger skill layouts', () => {
43
+ const source = new managed_skill_catalog_js_1.ManagedSkillCatalog({ rootDir: (0, node_path_1.join)(process.cwd(), 'electron', 'managed-skills') }).get('condition-trigger');
44
+ (0, strict_1.default)(source);
45
+ strict_1.default.equal(source.content, (0, node_fs_1.readFileSync)(sourceSkillPath, 'utf8'));
46
+ const packagedRoot = (0, node_path_1.join)(process.cwd(), 'dist-electron', 'electron', 'managed-skills');
47
+ strict_1.default.equal((0, node_fs_1.existsSync)((0, node_path_1.join)(packagedRoot, 'condition-trigger', 'SKILL.md')), true);
48
+ const packaged = new managed_skill_catalog_js_1.ManagedSkillCatalog({ rootDir: packagedRoot }).get('condition-trigger');
49
+ (0, strict_1.default)(packaged);
50
+ strict_1.default.equal(packaged.content, source.content);
51
+ });
52
+ (0, node_test_1.default)('condition trigger helper creates source-only staging input through a temporary file and rejects symlink roots', async () => {
53
+ const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'condition-trigger-skill-'));
54
+ const source = (0, node_path_1.join)(root, 'source');
55
+ const bin = (0, node_path_1.join)(root, 'bin');
56
+ const captureArgs = (0, node_path_1.join)(root, 'args');
57
+ const captureBody = (0, node_path_1.join)(root, 'body');
58
+ (0, node_fs_1.mkdirSync)(source);
59
+ (0, node_fs_1.mkdirSync)(bin);
60
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(source, 'manifest.json'), '{}');
61
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(source, 'check.js'), '#!/usr/bin/env node\n');
62
+ const fakeLac = (0, node_path_1.join)(bin, 'lac');
63
+ (0, node_fs_1.writeFileSync)(fakeLac, '#!/bin/sh\nprintf "%s\\n" "$@" > "$CAPTURE_ARGS"\nfor argument in "$@"; do last="$argument"; done\ncat "$last" > "$CAPTURE_BODY"\n');
64
+ (0, node_fs_1.chmodSync)(fakeLac, 0o755);
65
+ const helper = (0, node_path_1.join)(process.cwd(), 'electron', 'managed-skills', 'condition-trigger', 'scripts', 'register-condition-trigger.sh');
66
+ const run = (args) => new Promise((resolve, reject) => {
67
+ (0, node_child_process_1.execFile)(helper, args, {
68
+ env: { ...process.env, PATH: `${bin}:${process.env.PATH || ''}`, CAPTURE_ARGS: captureArgs, CAPTURE_BODY: captureBody },
69
+ }, (error) => error ? reject(error) : resolve());
70
+ });
71
+ try {
72
+ await run(['stage', 'script-1', source]);
73
+ const args = (0, node_fs_1.readFileSync)(captureArgs, 'utf8').trim().split('\n');
74
+ strict_1.default.deepEqual(args.slice(0, 5), ['script', 'stage', '--script', 'script-1', '--source-file']);
75
+ strict_1.default.equal((0, node_fs_1.existsSync)(args[5]), false, 'temporary serialized source is removed after lac exits');
76
+ strict_1.default.deepEqual(JSON.parse((0, node_fs_1.readFileSync)(captureBody, 'utf8')), [
77
+ { path: 'check.js', content: '#!/usr/bin/env node\n' },
78
+ { path: 'manifest.json', content: '{}' },
79
+ ]);
80
+ const symlink = (0, node_path_1.join)(root, 'source-link');
81
+ (0, node_fs_1.symlinkSync)(source, symlink);
82
+ await strict_1.default.rejects(run(['stage', 'script-1', symlink]), /source root must be a non-symlink directory/);
83
+ (0, node_fs_1.mkdirSync)((0, node_path_1.join)(source, 'nested'));
84
+ await strict_1.default.rejects(run(['stage', 'script-1', source]), /source bundle must be flat/);
85
+ (0, node_fs_1.rmSync)((0, node_path_1.join)(source, 'nested'), { recursive: true, force: true });
86
+ for (let index = 0; index < 65; index += 1)
87
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(source, `limit-${index}.txt`), 'x');
88
+ await strict_1.default.rejects(run(['stage', 'script-1', source]), /source bundle exceeds 64 files/);
89
+ }
90
+ finally {
91
+ (0, node_fs_1.rmSync)(root, { recursive: true, force: true });
92
+ }
93
+ });
@@ -110,6 +110,105 @@ class FakeEventSource {
110
110
  strict_1.default.deepEqual(events, ['presence.updated']);
111
111
  stop();
112
112
  });
113
+ (0, node_test_1.default)('core client accepts all four complete unified automation SSE events and rejects partial payloads', () => {
114
+ const source = new FakeEventSource();
115
+ const events = [];
116
+ const client = (0, client_js_1.createCoreClient)({
117
+ baseUrl: 'http://127.0.0.1:9851/api/local/v1',
118
+ eventSourceFactory: () => source,
119
+ });
120
+ const stop = client.events.subscribe((event) => events.push(event.type));
121
+ source.emit('automation.definition.updated', {
122
+ type: 'automation.definition.updated',
123
+ automation: completeAutomation(),
124
+ });
125
+ source.emit('automation.definition.updated', {
126
+ type: 'automation.definition.updated', automation: { ...completeAutomation(), health: 'blocked' },
127
+ });
128
+ source.emit('automation.evaluation.updated', {
129
+ type: 'automation.evaluation.updated',
130
+ evaluation: completeEvaluation(),
131
+ });
132
+ source.emit('automation.evaluation.updated', {
133
+ type: 'automation.evaluation.updated',
134
+ evaluation: { ...completeEvaluation(), conditionOutcome: 'matched', triggerDecision: 'not_evaluated' },
135
+ });
136
+ source.emit('automation.evaluation.updated', {
137
+ type: 'automation.evaluation.updated',
138
+ evaluation: { id: 'evaluation-running', automationId: 'automation-1', activationKind: 'interval', status: 'running', startedAt: '2026-07-12T00:00:00.000Z', finishedAt: '2026-07-12T00:00:01.000Z' },
139
+ });
140
+ source.emit('automation.run.updated', {
141
+ type: 'automation.run.updated',
142
+ run: completeAutomationRun(),
143
+ });
144
+ source.emit('automation.script-version.updated', {
145
+ type: 'automation.script-version.updated',
146
+ version: completeScriptVersion(),
147
+ });
148
+ source.emit('automation.definition.updated', {
149
+ type: 'automation.definition.updated',
150
+ automation: { ...completeAutomation(), condition: { kind: 'approved-script', scriptId: 'script-1', approvedVersionId: 'version-1', edge: 'falling' } },
151
+ });
152
+ source.emit('automation.evaluation.updated', {
153
+ type: 'automation.evaluation.updated',
154
+ evaluation: { ...completeEvaluation(), conditionOutcome: 'matched', triggerDecision: 'not_rising' },
155
+ });
156
+ source.emit('automation.run.updated', {
157
+ type: 'automation.run.updated', run: { ...completeAutomationRun(), status: 'unknown' },
158
+ });
159
+ source.emit('automation.script-version.updated', {
160
+ type: 'automation.script-version.updated', version: { ...completeScriptVersion(), env: ['TOKEN', 1] },
161
+ });
162
+ source.emit('automation.definition.updated', {
163
+ type: 'automation.definition.updated',
164
+ automation: { id: 'automation-1' },
165
+ });
166
+ source.emit('automation.script-version.updated', {
167
+ type: 'automation.script-version.updated',
168
+ version: { id: 'version-1', scriptId: 'script-1' },
169
+ });
170
+ strict_1.default.deepEqual(events, [
171
+ 'automation.definition.updated',
172
+ 'automation.evaluation.updated',
173
+ 'automation.run.updated',
174
+ 'automation.script-version.updated',
175
+ 'automation.evaluation.updated',
176
+ ]);
177
+ stop();
178
+ });
179
+ function completeAutomation() {
180
+ return {
181
+ id: 'automation-1', workspaceId: 'workspace-1', title: 'Automation', enabled: true, health: 'healthy',
182
+ activation: { kind: 'interval', intervalMs: 1_000 }, condition: { kind: 'always' },
183
+ action: { kind: 'agent-prompt', promptTemplate: 'hello', executionMode: 'same-thread' },
184
+ delivery: { platform: 'lark', route: { type: 'group', channelId: 'channel-1' } },
185
+ policies: { concurrency: 'skip-if-running', cooldownMs: 0 }, consecutiveEvaluationFailures: 0,
186
+ createdAt: '2026-07-12T00:00:00.000Z', updatedAt: '2026-07-12T00:00:00.000Z',
187
+ };
188
+ }
189
+ function completeEvaluation() {
190
+ return {
191
+ id: 'evaluation-1', automationId: 'automation-1', activationKind: 'interval', status: 'finished',
192
+ startedAt: '2026-07-12T00:00:00.000Z', finishedAt: '2026-07-12T00:00:01.000Z',
193
+ conditionOutcome: 'not_matched', triggerDecision: 'not_rising',
194
+ };
195
+ }
196
+ function completeAutomationRun() {
197
+ return {
198
+ id: 'run-1', automationId: 'automation-1', evaluationId: 'evaluation-1', status: 'succeeded',
199
+ executionMode: 'same-thread', createdAt: '2026-07-12T00:00:00.000Z',
200
+ };
201
+ }
202
+ function completeScriptVersion() {
203
+ return {
204
+ id: 'version-1', scriptId: 'script-1', status: 'approved', packageSha256: 'a'.repeat(64),
205
+ packagePath: '/managed/a', shebang: '#!/usr/bin/env node', interpreterPath: '/usr/bin/node', interpreterVersion: 'v22',
206
+ capabilities: {}, config: {}, configSchema: {}, networkMode: 'none', internalAccess: false,
207
+ allowedReadDirs: [], secretRefs: [], env: [],
208
+ limits: { timeoutMs: 30_000, stdoutBytes: 1_000, stderrBytes: 1_000, payloadBytes: 1_000, stateBytes: 1_000 },
209
+ staticCheck: {}, testPlan: {}, createdAt: '2026-07-12T00:00:00.000Z', updatedAt: '2026-07-12T00:00:00.000Z',
210
+ };
211
+ }
113
212
  (0, node_test_1.default)('core client rejects events whose nested payload misses required contract fields', () => {
114
213
  const source = new FakeEventSource();
115
214
  const events = [];
@@ -702,3 +702,113 @@ function withFetchMock(mock) {
702
702
  restore();
703
703
  }
704
704
  });
705
+ (0, node_test_1.default)('lac automation add verifies an approved script version before creating the automation', async () => {
706
+ const requests = [];
707
+ const { restore } = withFetchMock(async (input, init) => {
708
+ const url = String(input);
709
+ requests.push({ url, method: init?.method || 'GET', body: init?.body ? JSON.parse(String(init.body)) : undefined });
710
+ if (url.includes('/automation-scripts/versions/version-1')) {
711
+ return new Response(JSON.stringify({ ok: true, data: { id: 'version-1', scriptId: 'script-1', status: 'approved' } }), { headers: { 'content-type': 'application/json' } });
712
+ }
713
+ return new Response(JSON.stringify({ ok: true, data: { id: 'automation-1', title: 'Price watch' } }), { headers: { 'content-type': 'application/json' } });
714
+ });
715
+ try {
716
+ const { io } = createIo();
717
+ const exitCode = await (0, lac_js_1.runCli)([
718
+ 'automation', 'add', '--title', 'Price watch', '--script-version', 'version-1', '--script-id', 'script-1',
719
+ '--interval', '1m', '--message', 'Tell me when it matches', '--json',
720
+ ], {
721
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1',
722
+ LOCAL_AI_WORKSPACE_ID: 'workspace-1',
723
+ LOCAL_AI_THREAD_ID: 'thread-1',
724
+ LOCAL_AI_PLATFORM: 'lark',
725
+ LOCAL_AI_CHAT_ID: 'chat-1',
726
+ LOCAL_AI_PLATFORM_USER_ID: 'user-1',
727
+ }, io);
728
+ strict_1.default.equal(exitCode, 0);
729
+ strict_1.default.deepEqual(requests, [
730
+ { url: 'http://127.0.0.1:9831/api/local/v1/automation-scripts/versions/version-1?workspace_id=workspace-1', method: 'GET', body: undefined },
731
+ {
732
+ url: 'http://127.0.0.1:9831/api/local/v1/automations', method: 'POST', body: {
733
+ workspaceId: 'workspace-1', title: 'Price watch', enabled: true,
734
+ activation: { kind: 'interval', intervalMs: 60_000 },
735
+ condition: { kind: 'approved-script', scriptId: 'script-1', approvedVersionId: 'version-1', edge: 'rising' },
736
+ action: { kind: 'agent-prompt', promptTemplate: 'Tell me when it matches', executionMode: 'side-thread' },
737
+ delivery: { platform: 'lark', route: { type: 'channel.chat', channelId: 'chat-1', participantId: 'user-1', threadId: 'thread-1' } },
738
+ policies: { concurrency: 'skip-if-running', cooldownMs: 0 },
739
+ },
740
+ },
741
+ ]);
742
+ }
743
+ finally {
744
+ restore();
745
+ }
746
+ });
747
+ (0, node_test_1.default)('lac script stage uploads only a bounded source bundle and script test requires claimable authorization', async () => {
748
+ const requests = [];
749
+ const { restore } = withFetchMock(async (input, init) => {
750
+ const url = String(input);
751
+ requests.push({ url, method: init?.method || 'GET', body: init?.body ? JSON.parse(String(init.body)) : undefined });
752
+ if (url.includes('/versions/version-1') && !url.endsWith('/test')) {
753
+ return new Response(JSON.stringify({ ok: true, data: { id: 'version-1', status: 'test_authorized' } }), { headers: { 'content-type': 'application/json' } });
754
+ }
755
+ return new Response(JSON.stringify({ ok: true, data: { id: 'version-1', status: 'pending_test_approval' } }), { headers: { 'content-type': 'application/json' } });
756
+ });
757
+ try {
758
+ const { io } = createIo();
759
+ const env = { LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1', LOCAL_AI_WORKSPACE_ID: 'workspace-1', LOCAL_AI_THREAD_ID: 'thread-1' };
760
+ strict_1.default.equal(await (0, lac_js_1.runCli)(['script', 'stage', '--script', 'script-1', '--source-json', '[{"path":"manifest.json","content":"{}"},{"path":"check.js","content":"#!/usr/bin/env node"}]'], env, io), 0);
761
+ strict_1.default.deepEqual(requests[0], {
762
+ url: 'http://127.0.0.1:9831/api/local/v1/automation-scripts/script-1/versions?workspace_id=workspace-1', method: 'POST',
763
+ body: { files: [{ path: 'manifest.json', content: '{}' }, { path: 'check.js', content: '#!/usr/bin/env node' }] },
764
+ });
765
+ strict_1.default.equal(await (0, lac_js_1.runCli)(['script', 'test', 'version-1', '--actor', 'agent'], env, io), 0);
766
+ strict_1.default.deepEqual(requests.slice(1), [
767
+ { url: 'http://127.0.0.1:9831/api/local/v1/automation-scripts/versions/version-1?workspace_id=workspace-1', method: 'GET', body: undefined },
768
+ { url: 'http://127.0.0.1:9831/api/local/v1/automation-scripts/versions/version-1/test?workspace_id=workspace-1', method: 'POST', body: { actor: 'agent' } },
769
+ ]);
770
+ }
771
+ finally {
772
+ restore();
773
+ }
774
+ });
775
+ (0, node_test_1.default)('lac script and automation commands fail closed before a server-approved status permits execution', async () => {
776
+ const requests = [];
777
+ const { restore } = withFetchMock(async (input) => {
778
+ requests.push(String(input));
779
+ return new Response(JSON.stringify({ ok: true, data: { id: 'version-1', scriptId: 'script-1', status: 'pending_approval' } }), { headers: { 'content-type': 'application/json' } });
780
+ });
781
+ try {
782
+ const { io } = createIo();
783
+ const env = { LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1', LOCAL_AI_WORKSPACE_ID: 'workspace-1' };
784
+ strict_1.default.equal(await (0, lac_js_1.runCli)(['automation', 'add', '--title', 'blocked', '--script-id', 'script-1', '--script-version', 'version-1', '--message', 'nope'], env, io), 1);
785
+ strict_1.default.equal(await (0, lac_js_1.runCli)(['script', 'test', 'version-1', '--actor', 'agent'], env, io), 1);
786
+ strict_1.default.deepEqual(requests, [
787
+ 'http://127.0.0.1:9831/api/local/v1/automation-scripts/versions/version-1?workspace_id=workspace-1',
788
+ 'http://127.0.0.1:9831/api/local/v1/automation-scripts/versions/version-1?workspace_id=workspace-1',
789
+ ]);
790
+ }
791
+ finally {
792
+ restore();
793
+ }
794
+ });
795
+ (0, node_test_1.default)('lac automation add uses local delivery when channel identifiers are incomplete', async () => {
796
+ let body;
797
+ const { restore } = withFetchMock(async (input, init) => {
798
+ if (String(input).includes('/versions/version-1')) {
799
+ return new Response(JSON.stringify({ ok: true, data: { id: 'version-1', scriptId: 'script-1', status: 'approved' } }), { headers: { 'content-type': 'application/json' } });
800
+ }
801
+ body = JSON.parse(String(init?.body));
802
+ return new Response(JSON.stringify({ ok: true, data: { id: 'automation-1' } }), { headers: { 'content-type': 'application/json' } });
803
+ });
804
+ try {
805
+ const { io } = createIo();
806
+ strict_1.default.equal(await (0, lac_js_1.runCli)(['automation', 'add', '--title', 'local', '--script-id', 'script-1', '--script-version', 'version-1', '--message', 'check'], {
807
+ LOCAL_AI_CORE_BASE: 'http://127.0.0.1:9831/api/local/v1', LOCAL_AI_WORKSPACE_ID: 'workspace-1', LOCAL_AI_PLATFORM: 'lark',
808
+ }, io), 0);
809
+ strict_1.default.deepEqual(body?.delivery, { platform: 'local', route: { type: 'local.thread', channelId: 'workspace-1' } });
810
+ }
811
+ finally {
812
+ restore();
813
+ }
814
+ });
@@ -1003,6 +1003,31 @@ const env_utils_js_1 = require("../../services/local-ai-core/src/runtime/env-uti
1003
1003
  (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
1004
1004
  }
1005
1005
  });
1006
+ (0, node_test_1.default)('scheduler ignores malformed enabled legacy cron jobs during startup and ticks', async () => {
1007
+ const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-malformed-cron-'));
1008
+ const runtime = (0, bootstrap_js_1.bootstrapLocalCoreRuntime)({ userDataPath, enableKnowledge: false, log: () => { } });
1009
+ try {
1010
+ const job = runtime.store.createScheduledJob({
1011
+ workspaceId: 'workspace-a',
1012
+ platform: 'local',
1013
+ route: { type: 'local.thread', channelId: 'workspace-a', threadId: 'thread-1' },
1014
+ executionMode: 'side-thread',
1015
+ triggerType: 'cron',
1016
+ cronExpr: '* * * * *',
1017
+ promptTemplate: 'legacy',
1018
+ description: 'malformed legacy job',
1019
+ enabled: true,
1020
+ });
1021
+ runtime.store.db.prepare('UPDATE scheduled_jobs SET cron_expr = ? WHERE id = ?').run('*/0 * * * *', job.id);
1022
+ await runtime.scheduler.start();
1023
+ await runtime.scheduler.tick();
1024
+ strict_1.default.equal(runtime.store.listScheduledJobRuns(job.id).length, 0);
1025
+ }
1026
+ finally {
1027
+ await runtime.stop();
1028
+ (0, node_fs_1.rmSync)(userDataPath, { recursive: true, force: true });
1029
+ }
1030
+ });
1006
1031
  (0, node_test_1.default)('scheduler auto-disables a job after 5 consecutive failures', async () => {
1007
1032
  const userDataPath = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'scheduler-autodisable-'));
1008
1033
  try {
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const strict_1 = __importDefault(require("node:assert/strict"));
7
+ const node_test_1 = __importDefault(require("node:test"));
8
+ const node_stream_1 = require("node:stream");
9
+ const server_routes_js_1 = require("../../services/local-ai-core/src/runtime/server-routes.js");
10
+ const automations_handler_js_1 = require("../../services/local-ai-core/src/runtime/handlers/automations-handler.js");
11
+ (0, node_test_1.default)('unified automation routes are explicit and do not overlap legacy monitor routes', () => {
12
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/automations'), { name: 'automations.list' });
13
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automations'), { name: 'automations.create' });
14
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/automations/automation%2Fone'), {
15
+ name: 'automation.get', automationId: 'automation/one',
16
+ });
17
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('PATCH', '/api/local/v1/automations/automation-1'), {
18
+ name: 'automation.update', automationId: 'automation-1',
19
+ });
20
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('DELETE', '/api/local/v1/automations/automation-1'), {
21
+ name: 'automation.delete', automationId: 'automation-1',
22
+ });
23
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automations/automation-1/check'), {
24
+ name: 'automation.check', automationId: 'automation-1',
25
+ });
26
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/automations/automation-1/evaluations'), {
27
+ name: 'automation.evaluations', automationId: 'automation-1',
28
+ });
29
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/automations/automation-1/runs'), {
30
+ name: 'automation.runs', automationId: 'automation-1',
31
+ });
32
+ strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/automations/automation-1/check'), null);
33
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/automation/monitors'), { name: 'automation.monitors.list' });
34
+ });
35
+ (0, node_test_1.default)('unified script routes expose only lifecycle transitions', () => {
36
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/automation-scripts'), { name: 'automation-scripts.list' });
37
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts'), { name: 'automation-scripts.create' });
38
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('GET', '/api/local/v1/automation-scripts/script-1/versions'), {
39
+ name: 'automation-script.versions', scriptId: 'script-1',
40
+ });
41
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts/script-1/versions'), {
42
+ name: 'automation-script.version.submit', scriptId: 'script-1',
43
+ });
44
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts/versions/version-1/test-approval'), {
45
+ name: 'automation-script-version.test-approval', versionId: 'version-1',
46
+ });
47
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts/versions/version-1/test'), {
48
+ name: 'automation-script-version.test', versionId: 'version-1',
49
+ });
50
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts/versions/version-1/enable-approval'), {
51
+ name: 'automation-script-version.enable-approval', versionId: 'version-1',
52
+ });
53
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts/versions/version-1/approve'), {
54
+ name: 'automation-script-version.approve', versionId: 'version-1',
55
+ });
56
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts/versions/version-1/reject'), {
57
+ name: 'automation-script-version.reject', versionId: 'version-1',
58
+ });
59
+ strict_1.default.deepEqual((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts/versions/version-1/revoke'), {
60
+ name: 'automation-script-version.revoke', versionId: 'version-1',
61
+ });
62
+ strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts/versions/version-1/stage'), null);
63
+ strict_1.default.equal((0, server_routes_js_1.parseLocalAiCoreRoute)('POST', '/api/local/v1/automation-scripts/versions/version-1/approve/extra'), null);
64
+ });
65
+ (0, node_test_1.default)('unified handlers enforce workspace ownership and reject server-owned fields', async () => {
66
+ const map = new Map();
67
+ (0, automations_handler_js_1.registerUnifiedAutomationHandlers)(map, {
68
+ automations: {
69
+ get: () => ({ id: 'automation-1', workspaceId: 'workspace-a' }),
70
+ create: () => { throw new Error('must not create'); },
71
+ },
72
+ store: { getAutomationScript: () => ({ id: 'script-1', workspaceId: 'workspace-a' }) },
73
+ });
74
+ await strict_1.default.rejects(map.get('automation.get')({ name: 'automation.get', automationId: 'automation-1' }, node_stream_1.Readable.from([]), response(), new URL('http://127.0.0.1/automations/automation-1?workspace_id=workspace-b')), /not found in this workspace/);
75
+ await strict_1.default.rejects(map.get('automations.create')({ name: 'automations.create' }, requestBody({
76
+ workspaceId: 'workspace-a', title: 'Automation', enabled: true,
77
+ activation: { kind: 'interval', intervalMs: 1000 }, condition: { kind: 'always' },
78
+ action: { kind: 'agent-prompt', promptTemplate: 'hello', executionMode: 'same-thread' },
79
+ delivery: { platform: 'lark', route: { type: 'group', channelId: 'channel-1' } },
80
+ policies: { concurrency: 'skip-if-running', cooldownMs: 0 }, health: 'healthy',
81
+ }), response(), new URL('http://127.0.0.1/automations')), /health is not writable/);
82
+ await strict_1.default.rejects(map.get('automation-scripts.create')({ name: 'automation-scripts.create' }, requestBody({ workspaceId: 'workspace-a', title: 'Script', packagePath: '/tmp/attacker-controlled' }), response(), new URL('http://127.0.0.1/automation-scripts')), /packagePath is not writable/);
83
+ await strict_1.default.rejects(map.get('automation-script.version.submit')({ name: 'automation-script.version.submit', scriptId: 'script-1' }, requestBody({ files: [], sourceDir: '/tmp/attacker-controlled' }), response(), new URL('http://127.0.0.1/automation-scripts/script-1/versions?workspace_id=workspace-a')), /sourceDir is not writable/);
84
+ });
85
+ (0, node_test_1.default)('script approval lifecycle selects the transition from the server-owned version state', async () => {
86
+ const calls = [];
87
+ let status = 'pending_test_approval';
88
+ const version = () => ({ id: 'version-1', scriptId: 'script-1', status });
89
+ const map = new Map();
90
+ (0, automations_handler_js_1.registerUnifiedAutomationHandlers)(map, {
91
+ automations: {},
92
+ store: {
93
+ getAutomationScriptVersion: () => version(),
94
+ getAutomationScript: () => ({ id: 'script-1', workspaceId: 'workspace-a' }),
95
+ getApprovalRequest: (approvalId) => ({ approvalId, status: approvalId === 'approval-test' ? 'approved' : 'rejected' }),
96
+ authorizeAutomationScriptTest: () => {
97
+ calls.push('authorize-test');
98
+ return version();
99
+ },
100
+ approveAutomationScriptVersion: () => {
101
+ calls.push('approve-enable');
102
+ return version();
103
+ },
104
+ },
105
+ });
106
+ await map.get('automation-script-version.approve')({ name: 'automation-script-version.approve', versionId: 'version-1' }, requestBody({ approvalId: 'approval-test', actor: 'user' }), response(), new URL('http://127.0.0.1/automation-scripts/versions/version-1/approve?workspace_id=workspace-a'));
107
+ status = 'pending_approval';
108
+ await map.get('automation-script-version.reject')({ name: 'automation-script-version.reject', versionId: 'version-1' }, requestBody({ approvalId: 'approval-enable', actor: 'user' }), response(), new URL('http://127.0.0.1/automation-scripts/versions/version-1/reject?workspace_id=workspace-a'));
109
+ strict_1.default.deepEqual(calls, ['authorize-test', 'approve-enable']);
110
+ status = 'pending_test_approval';
111
+ await strict_1.default.rejects(map.get('automation-script-version.reject')({ name: 'automation-script-version.reject', versionId: 'version-1' }, requestBody({ approvalId: 'approval-test', actor: 'user' }), response(), new URL('http://127.0.0.1/automation-scripts/versions/version-1/reject?workspace_id=workspace-a')), /requires a rejected approval decision/);
112
+ strict_1.default.deepEqual(calls, ['authorize-test', 'approve-enable']);
113
+ });
114
+ (0, node_test_1.default)('script test execution accepts only an actor and persists the server-produced report', async () => {
115
+ const reports = [];
116
+ const version = { id: 'version-1', scriptId: 'script-1', status: 'test_authorized' };
117
+ const map = new Map();
118
+ (0, automations_handler_js_1.registerUnifiedAutomationHandlers)(map, {
119
+ automations: {},
120
+ store: {
121
+ getAutomationScriptVersion: () => version,
122
+ getAutomationScript: () => ({ id: 'script-1', workspaceId: 'workspace-a' }),
123
+ claimAutomationScriptTestExecution: () => ({ ...version, status: 'testing' }),
124
+ recordAutomationScriptTestResult: (_versionId, report) => {
125
+ reports.push(report);
126
+ return { ...version, status: 'tested' };
127
+ },
128
+ },
129
+ executeScriptTest: async () => ({ status: 'passed', finishedAt: '2026-07-12T00:00:00.000Z', summary: 'server test' }),
130
+ });
131
+ await map.get('automation-script-version.test')({ name: 'automation-script-version.test', versionId: 'version-1' }, requestBody({ actor: 'user' }), response(), new URL('http://127.0.0.1/automation-scripts/versions/version-1/test?workspace_id=workspace-a'));
132
+ strict_1.default.deepEqual(reports, [{ status: 'passed', finishedAt: '2026-07-12T00:00:00.000Z', summary: 'server test' }]);
133
+ await strict_1.default.rejects(map.get('automation-script-version.test')({ name: 'automation-script-version.test', versionId: 'version-1' }, requestBody({ actor: 'user', testReport: { status: 'passed' } }), response(), new URL('http://127.0.0.1/automation-scripts/versions/version-1/test?workspace_id=workspace-a')), /testReport is not writable/);
134
+ });
135
+ (0, node_test_1.default)('only one concurrent request can claim a one-shot script test before sandbox execution', async () => {
136
+ let status = 'test_authorized';
137
+ let resolveExecution;
138
+ const execution = new Promise((resolve) => { resolveExecution = resolve; });
139
+ let executions = 0;
140
+ const version = () => ({ id: 'version-1', scriptId: 'script-1', status });
141
+ const map = new Map();
142
+ (0, automations_handler_js_1.registerUnifiedAutomationHandlers)(map, {
143
+ automations: {},
144
+ store: {
145
+ getAutomationScriptVersion: () => version(),
146
+ getAutomationScript: () => ({ id: 'script-1', workspaceId: 'workspace-a' }),
147
+ claimAutomationScriptTestExecution: () => {
148
+ if (status !== 'test_authorized')
149
+ throw new Error(`claim requires test_authorized, got ${status}`);
150
+ status = 'testing';
151
+ return version();
152
+ },
153
+ recordAutomationScriptTestResult: () => ({ ...version(), status: 'tested' }),
154
+ },
155
+ executeScriptTest: async () => {
156
+ executions += 1;
157
+ await execution;
158
+ return { status: 'passed', finishedAt: '2026-07-12T00:00:00.000Z' };
159
+ },
160
+ });
161
+ const route = { name: 'automation-script-version.test', versionId: 'version-1' };
162
+ const url = new URL('http://127.0.0.1/automation-scripts/versions/version-1/test?workspace_id=workspace-a');
163
+ const first = map.get('automation-script-version.test')(route, requestBody({ actor: 'user' }), response(), url);
164
+ await Promise.resolve();
165
+ await strict_1.default.rejects(map.get('automation-script-version.test')(route, requestBody({ actor: 'user' }), response(), url), /already executing/);
166
+ strict_1.default.equal(executions, 1);
167
+ resolveExecution();
168
+ await first;
169
+ });
170
+ (0, node_test_1.default)('source submission rejects an oversized body before JSON parsing or package staging', async () => {
171
+ const map = new Map();
172
+ (0, automations_handler_js_1.registerUnifiedAutomationHandlers)(map, {
173
+ automations: {},
174
+ store: { getAutomationScript: () => ({ id: 'script-1', workspaceId: 'workspace-a' }) },
175
+ });
176
+ await strict_1.default.rejects(map.get('automation-script.version.submit')({ name: 'automation-script.version.submit', scriptId: 'script-1' }, node_stream_1.Readable.from([Buffer.alloc(1_200_001)]), response(), new URL('http://127.0.0.1/automation-scripts/script-1/versions?workspace_id=workspace-a')), /exceeds 1200000 bytes/);
177
+ });
178
+ function requestBody(value) {
179
+ return node_stream_1.Readable.from([Buffer.from(JSON.stringify(value))]);
180
+ }
181
+ function response() {
182
+ return {
183
+ setHeader() { },
184
+ end() { },
185
+ };
186
+ }
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_test_1 = __importDefault(require("node:test"));
7
+ const strict_1 = __importDefault(require("node:assert/strict"));
8
+ const node_fs_1 = require("node:fs");
9
+ const node_os_1 = require("node:os");
10
+ const node_path_1 = require("node:path");
11
+ const anthropic_sandbox_runner_js_1 = require("../../services/local-ai-core/src/automation/scripts/anthropic-sandbox-runner.js");
12
+ const root = (0, node_fs_1.mkdtempSync)((0, node_path_1.join)((0, node_os_1.tmpdir)(), 'automation-sandbox-runtime-'));
13
+ const packagePath = (0, node_path_1.join)(root, 'package');
14
+ const tempDir = (0, node_path_1.join)(root, 'tmp');
15
+ const secretPath = (0, node_path_1.join)(root, 'fixture-secret.txt');
16
+ (0, node_fs_1.mkdirSync)(packagePath);
17
+ (0, node_fs_1.mkdirSync)(tempDir);
18
+ (0, node_fs_1.writeFileSync)((0, node_path_1.join)(packagePath, 'entry.js'), '#!/usr/bin/env node\n');
19
+ (0, node_fs_1.chmodSync)((0, node_path_1.join)(packagePath, 'entry.js'), 0o444);
20
+ (0, node_fs_1.writeFileSync)(secretPath, 'fixture-secret-value');
21
+ const runner = new anthropic_sandbox_runner_js_1.AnthropicSandboxRunner({ tempRoot: root });
22
+ const nodeCommand = process.execPath;
23
+ (0, node_test_1.default)('real Anthropic Sandbox Runtime enforces package, temp, and network policy', async (context) => {
24
+ const capability = await runner.probe();
25
+ const outboundUnavailable = process.env.AGENTDOCK_TEST_NO_OUTBOUND === '1';
26
+ const skipReason = !capability.available
27
+ ? `sandbox capability unavailable: ${capability.missing.join(', ')}`
28
+ : outboundUnavailable
29
+ ? 'outbound networking explicitly unavailable by AGENTDOCK_TEST_NO_OUTBOUND=1'
30
+ : undefined;
31
+ if (skipReason) {
32
+ context.skip(skipReason);
33
+ (0, node_fs_1.rmSync)(root, { recursive: true, force: true });
34
+ return;
35
+ }
36
+ try {
37
+ const write = await runner.run({
38
+ command: `${nodeCommand} -e ${JSON.stringify("require('fs').writeFileSync('" + tempDir + "/written.txt','ok')")}`,
39
+ cwd: root,
40
+ packagePath,
41
+ tempDir,
42
+ network: 'none',
43
+ denyRead: [secretPath],
44
+ });
45
+ strict_1.default.equal(write.exitCode, 0, write.stderr);
46
+ strict_1.default.equal((0, node_fs_1.readFileSync)((0, node_path_1.join)(tempDir, 'written.txt'), 'utf8'), 'ok');
47
+ const secretRead = await runner.run({
48
+ command: `${nodeCommand} -e ${JSON.stringify("const value=require('fs').readFileSync('" + secretPath + "','utf8'); process.exit(value==='fixture-secret-value'?0:1)")}`,
49
+ cwd: root,
50
+ packagePath,
51
+ tempDir,
52
+ network: 'none',
53
+ denyRead: [secretPath],
54
+ });
55
+ strict_1.default.notEqual(secretRead.exitCode, 0);
56
+ const packageWrite = await runner.run({
57
+ command: `${nodeCommand} -e ${JSON.stringify("require('fs').writeFileSync('" + packagePath + "/unexpected.txt','no')")}`,
58
+ cwd: root,
59
+ packagePath,
60
+ tempDir,
61
+ network: 'none',
62
+ });
63
+ strict_1.default.notEqual(packageWrite.exitCode, 0);
64
+ const defaultTempWrite = await runner.run({
65
+ command: `${nodeCommand} -e ${JSON.stringify("require('fs').writeFileSync('/tmp/claude/unexpected.txt','no')")}`,
66
+ cwd: root,
67
+ packagePath,
68
+ tempDir,
69
+ network: 'none',
70
+ });
71
+ strict_1.default.notEqual(defaultTempWrite.exitCode, 0);
72
+ const publicEgress = await runner.run({
73
+ command: 'curl -fsS https://example.com >/dev/null',
74
+ cwd: root,
75
+ packagePath,
76
+ tempDir,
77
+ network: 'public',
78
+ });
79
+ strict_1.default.equal(publicEgress.exitCode, 0, publicEgress.stderr);
80
+ const privateEgress = await runner.run({
81
+ command: 'curl --noproxy \'\' -fsS --connect-timeout 2 http://127.0.0.1:9 >/dev/null',
82
+ cwd: root,
83
+ packagePath,
84
+ tempDir,
85
+ network: 'public',
86
+ });
87
+ strict_1.default.notEqual(privateEgress.exitCode, 0);
88
+ }
89
+ finally {
90
+ (0, node_fs_1.rmSync)(root, { recursive: true, force: true });
91
+ }
92
+ });