blun-king-cli 9.1.536 → 9.1.561
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/LIESMICH.txt +13 -869
- package/README.md +41 -833
- package/bin/assistant-message-offload-policy.cjs +3 -1
- package/bin/compaction-transaction-policy.cjs +122 -0
- package/bin/context-performance-policy.cjs +2 -5
- package/bin/context-pressure-policy.cjs +20 -0
- package/bin/cron-run-output.cjs +45 -0
- package/bin/cron-run-store.cjs +145 -0
- package/bin/default-model-output-budget-policy.cjs +28 -0
- package/bin/durable-task-resume-policy.cjs +130 -0
- package/bin/durable-task-resume-runtime.cjs +117 -0
- package/bin/durable-task-resume-store.cjs +88 -0
- package/bin/editable-tool-approval-policy.cjs +540 -0
- package/bin/editable-tool-approval-runtime.cjs +99 -0
- package/bin/file-observation-policy.cjs +133 -0
- package/bin/html-to-research-markdown.cjs +146 -0
- package/bin/launcher-runtime.js +0 -1
- package/bin/micro-compaction-policy.cjs +64 -0
- package/bin/mnemo-connect-heartbeat.cjs +1 -3
- package/bin/programmatic-tool-runtime.mjs +330 -4
- package/bin/read-continuation-policy.cjs +36 -5
- package/bin/retry-checkpoint-policy.cjs +13 -0
- package/bin/scoped-cron-run-policy.cjs +358 -0
- package/bin/session-checkpoint-policy.cjs +25 -0
- package/bin/startup-preferences.cjs +4 -3
- package/bin/structured-agent-swarm-output.cjs +325 -0
- package/bin/subagent-context-fork-policy.cjs +155 -0
- package/bin/subagent-skill-policy.cjs +204 -0
- package/bin/telegram-approval-relay.cjs +2 -1
- package/bin/tool-file-persistence.cjs +141 -0
- package/bin/tool-result-offload-policy.cjs +25 -33
- package/bin/turn-thinking-policy.cjs +2 -26
- package/bin/update-notice.js +30 -18
- package/bin/user-message-offload-policy.cjs +3 -1
- package/blun.mjs +1564 -640
- package/codebase-index/README.md +12 -0
- package/codebase-index/codebase_index.py +129 -18
- package/package.json +23 -58
- package/telegram-plugin/bin/telegram-mnemo-capture.cjs +1 -3
- package/telegram-plugin/bin/telegram-typing-keepalive.cjs +89 -0
- package/telegram-plugin/dist/bridge.mjs +8 -1
- package/CHANGELOG.md +0 -321
- package/agent-spine-plugin/CHANGELOG.md +0 -406
- package/agent-spine-plugin/CONTRIBUTING.md +0 -52
- package/agent-spine-plugin/README.md +0 -344
- package/agent-spine-plugin/SECURITY.md +0 -47
- package/agent-spine-plugin/docs/acceptance.md +0 -61
- package/agent-spine-plugin/docs/architecture.md +0 -183
- package/agent-spine-plugin/docs/attention.md +0 -121
- package/agent-spine-plugin/docs/automatic-continuity.md +0 -79
- package/agent-spine-plugin/docs/channel-runtime.md +0 -92
- package/agent-spine-plugin/docs/coordination.md +0 -138
- package/agent-spine-plugin/docs/feed-transport.md +0 -99
- package/agent-spine-plugin/docs/gateway-runtime.md +0 -116
- package/agent-spine-plugin/docs/harness-reference.md +0 -45
- package/agent-spine-plugin/docs/host-integration.md +0 -129
- package/agent-spine-plugin/docs/https-transport.md +0 -116
- package/agent-spine-plugin/docs/learning.md +0 -133
- package/agent-spine-plugin/docs/object-transport.md +0 -93
- package/agent-spine-plugin/docs/peer-transport.md +0 -88
- package/agent-spine-plugin/docs/preflight-recall.md +0 -69
- package/agent-spine-plugin/docs/preservation-contract.md +0 -53
- package/agent-spine-plugin/docs/quality-gates.md +0 -50
- package/agent-spine-plugin/docs/relationships.md +0 -73
- package/agent-spine-plugin/docs/releasing.md +0 -83
- package/agent-spine-plugin/docs/roadmap.md +0 -307
- package/agent-spine-plugin/docs/selfstarter.md +0 -88
- package/agent-spine-plugin/docs/session-briefing.md +0 -74
- package/agent-spine-plugin/docs/shared-memory.md +0 -259
- package/agent-spine-plugin/docs/source-roots.md +0 -86
- package/agent-spine-plugin/docs/sqlite-transport.md +0 -76
- package/agent-spine-plugin/scripts/check-hosts.js +0 -195
- package/agent-spine-plugin/scripts/check-install.js +0 -569
- package/agent-spine-plugin/scripts/check-syntax.js +0 -29
- package/agent-spine-plugin/scripts/github-actions.js +0 -11
- package/agent-spine-plugin/scripts/release-check.js +0 -128
- package/agent-spine-plugin/scripts/run-acceptance.js +0 -19
- package/agent-spine-plugin/scripts/run-checks.js +0 -46
- package/agent-spine-plugin/scripts/run-tests-hermetic.js +0 -73
- package/agent-spine-plugin/spine-example/1-identity.md +0 -12
- package/agent-spine-plugin/spine-example/2-voice.md +0 -6
- package/agent-spine-plugin/spine-example/3-conduct.md +0 -8
- package/agent-spine-plugin/spine-example/4-history.md +0 -4
- package/bin/empty-response-retry-policy.cjs +0 -29
- package/bin/fredrik-glm-provider.cjs +0 -256
- package/bin/package-regression-policy.cjs +0 -77
- package/fredrik-glm-profile.toml.example +0 -26
- package/release-planned-removals.json +0 -15
- package/scripts/check-active-profile-plugin-startup.js +0 -36
- package/scripts/check-active-work-steer-regression.js +0 -46
- package/scripts/check-approval-observability-regression.js +0 -111
- package/scripts/check-approval-queue-shortcuts-regression.js +0 -65
- package/scripts/check-bundled-agent-spine-regression.js +0 -48
- package/scripts/check-codebase-search-packaging-regression.js +0 -92
- package/scripts/check-copy-command-regression.js +0 -74
- package/scripts/check-current-turn-read-pin-mutation-regression.js +0 -72
- package/scripts/check-current-turn-read-pin-regression.js +0 -94
- package/scripts/check-deepseek-native-max-regression.js +0 -49
- package/scripts/check-empty-response-effort-downgrade-regression.js +0 -48
- package/scripts/check-fredrik-glm-mutation-regression.js +0 -18
- package/scripts/check-fredrik-glm-regression.js +0 -169
- package/scripts/check-historical-tool-result-preview-regression.js +0 -77
- package/scripts/check-history-pressure-offload-regression.js +0 -77
- package/scripts/check-mcp-startup-wait-budget.js +0 -48
- package/scripts/check-package-regression.js +0 -38
- package/scripts/check-plugin-startup-regression.js +0 -53
- package/scripts/check-programmatic-context-isolation-regression.js +0 -193
- package/scripts/check-programmatic-tool-regression.js +0 -294
- package/scripts/check-queue-controls-regression.js +0 -189
- package/scripts/check-release-metadata.js +0 -103
- package/scripts/check-reload-agent-spine-regression.js +0 -76
- package/scripts/check-resume-replay-regression.js +0 -102
- package/scripts/check-session-cancel-regression.js +0 -43
- package/scripts/check-session-picker-resume-metrics-regression.js +0 -97
- package/scripts/check-session-start-hook-context-regression.js +0 -228
- package/scripts/check-shell-terminal-isolation-regression.js +0 -81
- package/scripts/check-slash-escape-regression.js +0 -89
- package/scripts/check-startup-swarm-command-regression.js +0 -24
- package/scripts/check-structured-subagent-output-regression.js +0 -331
- package/scripts/check-telegram-bridge-watchdog.js +0 -60
- package/scripts/check-telegram-direct-work-resume-regression.js +0 -53
- package/scripts/check-telegram-loop-exactly-once-regression.js +0 -71
- package/scripts/check-todo-loop-regression.js +0 -78
- package/scripts/check-todo-progress-regression.js +0 -416
- package/scripts/check-todo-recovery-catalog-regression.js +0 -50
- package/scripts/check-tool-schema-capacity-regression.js +0 -40
- package/scripts/programmatic-tool-runtime.test.mjs +0 -365
- package/scripts/structured-subagent-output.test.cjs +0 -170
- /package/{scripts → bin}/fix-node-pty-perms.js +0 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const crypto = require('node:crypto');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const MAX_ALLOWED_TOOLS = 64;
|
|
7
|
+
const MAX_TOOL_NAME_BYTES = 160;
|
|
8
|
+
const MAX_RECEIPT_TOOL_NAMES = 64;
|
|
9
|
+
const MAX_RECEIPT_OUTPUTS = 32;
|
|
10
|
+
const MAX_LIST_LIMIT = 100;
|
|
11
|
+
const TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled', 'lost']);
|
|
12
|
+
const ALL_STATUSES = new Set(['buffered', 'running', ...TERMINAL_STATUSES]);
|
|
13
|
+
const TRANSITIONS = new Map([
|
|
14
|
+
['buffered', new Set(['running', 'failed', 'cancelled', 'lost'])],
|
|
15
|
+
['running', new Set(['completed', 'failed', 'cancelled', 'lost'])],
|
|
16
|
+
]);
|
|
17
|
+
const ERROR_CATEGORIES = new Set(['executor', 'provider', 'policy', 'unknown']);
|
|
18
|
+
|
|
19
|
+
class ScopedCronRunError extends TypeError {
|
|
20
|
+
constructor(code, message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = 'ScopedCronRunError';
|
|
23
|
+
this.code = code;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function fail(code, message) {
|
|
28
|
+
throw new ScopedCronRunError(code, message);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function clone(value) {
|
|
32
|
+
return JSON.parse(JSON.stringify(value));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function deepFreeze(value) {
|
|
36
|
+
if (!value || typeof value !== 'object' || Object.isFrozen(value)) return value;
|
|
37
|
+
for (const child of Object.values(value)) deepFreeze(child);
|
|
38
|
+
return Object.freeze(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function assertOpaqueId(value, field) {
|
|
42
|
+
if (typeof value !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value)) {
|
|
43
|
+
fail('CRON_RUN_INVALID_ID', `${field} must be a bounded opaque identifier`);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function canonicalToolName(value, field) {
|
|
49
|
+
if (
|
|
50
|
+
typeof value !== 'string'
|
|
51
|
+
|| value.length === 0
|
|
52
|
+
|| Buffer.byteLength(value, 'utf8') > MAX_TOOL_NAME_BYTES
|
|
53
|
+
|| /[*?[\]{}]/u.test(value)
|
|
54
|
+
|| !/^[A-Za-z0-9][A-Za-z0-9._:/-]*$/u.test(value)
|
|
55
|
+
) {
|
|
56
|
+
fail('CRON_SCOPE_INVALID_TOOL', `${field} must be an exact canonical tool name`);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function canonicalToolNames(tools, field = 'tools') {
|
|
62
|
+
if (!Array.isArray(tools)) fail('CRON_SCOPE_INVALID_CATALOG', `${field} must be an array`);
|
|
63
|
+
const names = tools.map((entry) => canonicalToolName(
|
|
64
|
+
typeof entry === 'string' ? entry : entry && entry.name,
|
|
65
|
+
field,
|
|
66
|
+
));
|
|
67
|
+
return [...new Set(names)];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function cronToolScopeDigest(names) {
|
|
71
|
+
return `sha256:${crypto.createHash('sha256').update(JSON.stringify(names)).digest('hex')}`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeCronAllowedTools(input, effectiveTools) {
|
|
75
|
+
if (input === undefined) return Object.freeze({mode: 'legacy', names: null, digest: null});
|
|
76
|
+
if (!Array.isArray(input)) fail('CRON_SCOPE_NOT_ARRAY', 'allowed_tools must be an array');
|
|
77
|
+
if (input.length > MAX_ALLOWED_TOOLS) {
|
|
78
|
+
fail('CRON_SCOPE_TOO_MANY_TOOLS', `allowed_tools supports at most ${MAX_ALLOWED_TOOLS} entries`);
|
|
79
|
+
}
|
|
80
|
+
const catalog = new Set(canonicalToolNames(effectiveTools, 'effective tools'));
|
|
81
|
+
const names = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
for (const rawName of input) {
|
|
84
|
+
const name = canonicalToolName(rawName, 'allowed_tools entry');
|
|
85
|
+
if (!catalog.has(name)) fail('CRON_SCOPE_UNKNOWN_TOOL', `Unknown current tool: ${name}`);
|
|
86
|
+
if (seen.has(name)) continue;
|
|
87
|
+
seen.add(name);
|
|
88
|
+
names.push(name);
|
|
89
|
+
}
|
|
90
|
+
return deepFreeze({mode: 'scoped', names, digest: cronToolScopeDigest(names)});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cronToolScopeForTask(task) {
|
|
94
|
+
if (!task || task.allowedTools === undefined) {
|
|
95
|
+
return Object.freeze({mode: 'legacy', names: null, digest: null});
|
|
96
|
+
}
|
|
97
|
+
if (!Array.isArray(task.allowedTools)) fail('CRON_SCOPE_INVALID_TASK', 'Persisted allowedTools must be an array');
|
|
98
|
+
const names = canonicalToolNames(task.allowedTools, 'persisted allowedTools');
|
|
99
|
+
const digest = cronToolScopeDigest(names);
|
|
100
|
+
if (task.toolScopeDigest !== digest) fail('CRON_SCOPE_DIGEST_MISMATCH', 'Persisted tool scope digest does not match');
|
|
101
|
+
return deepFreeze({mode: 'scoped', names, digest});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isCronToolScope(value) {
|
|
105
|
+
if (!value || value.mode !== 'scoped' || !Array.isArray(value.names)) return false;
|
|
106
|
+
try {
|
|
107
|
+
const names = canonicalToolNames(value.names, 'tool scope');
|
|
108
|
+
return names.length === value.names.length && value.digest === cronToolScopeDigest(names);
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function filterToolsForCronScope(tools, scope) {
|
|
115
|
+
if (!Array.isArray(tools)) fail('CRON_SCOPE_INVALID_CATALOG', 'tools must be an array');
|
|
116
|
+
if (!scope || scope.mode === 'legacy') return tools;
|
|
117
|
+
if (!isCronToolScope(scope)) fail('CRON_SCOPE_INVALID_ORIGIN', 'Invalid cron tool scope');
|
|
118
|
+
const allowed = new Set(scope.names);
|
|
119
|
+
return tools.filter((tool) => tool && allowed.has(tool.name));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function inheritedCronToolScope(parentScope) {
|
|
123
|
+
if (!parentScope || parentScope.mode === 'legacy') return undefined;
|
|
124
|
+
if (!isCronToolScope(parentScope)) fail('CRON_SCOPE_INVALID_ORIGIN', 'Invalid inherited cron tool scope');
|
|
125
|
+
return deepFreeze({mode: 'scoped', names: [...parentScope.names], digest: parentScope.digest});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function isoTimestamp(value, field) {
|
|
129
|
+
if (typeof value !== 'string' || !Number.isFinite(Date.parse(value))) {
|
|
130
|
+
fail('CRON_RUN_INVALID_TIME', `${field} must be an ISO timestamp`);
|
|
131
|
+
}
|
|
132
|
+
return value;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function createCronRunReceipt(input) {
|
|
136
|
+
const digest = input.toolScopeDigest;
|
|
137
|
+
if (digest !== null && (typeof digest !== 'string' || !/^sha256:[a-f0-9]{64}$/u.test(digest))) {
|
|
138
|
+
fail('CRON_RUN_INVALID_DIGEST', 'toolScopeDigest is invalid');
|
|
139
|
+
}
|
|
140
|
+
if (!Number.isSafeInteger(input.coalescedCount) || input.coalescedCount < 1) {
|
|
141
|
+
fail('CRON_RUN_INVALID_COALESCE', 'coalescedCount must be a positive integer');
|
|
142
|
+
}
|
|
143
|
+
return deepFreeze({
|
|
144
|
+
runId: assertOpaqueId(input.runId, 'runId'),
|
|
145
|
+
jobId: assertOpaqueId(input.jobId, 'jobId'),
|
|
146
|
+
jobDeleted: false,
|
|
147
|
+
cron: String(input.cron),
|
|
148
|
+
recurring: input.recurring === true,
|
|
149
|
+
stale: input.stale === true,
|
|
150
|
+
coalescedCount: input.coalescedCount,
|
|
151
|
+
toolScopeDigest: digest,
|
|
152
|
+
status: 'buffered',
|
|
153
|
+
firedAt: isoTimestamp(input.firedAt, 'firedAt'),
|
|
154
|
+
startedAt: null,
|
|
155
|
+
endedAt: null,
|
|
156
|
+
turnId: null,
|
|
157
|
+
toolSummary: {calls: 0, errors: 0, names: [], excessNames: 0},
|
|
158
|
+
outputs: [],
|
|
159
|
+
finalMessageRef: null,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function summarizeCronToolCalls(toolCalls) {
|
|
164
|
+
if (!Array.isArray(toolCalls)) fail('CRON_RUN_INVALID_TOOLS', 'toolCalls must be an array');
|
|
165
|
+
const names = new Set();
|
|
166
|
+
let errors = 0;
|
|
167
|
+
for (const call of toolCalls) {
|
|
168
|
+
canonicalToolName(call && call.name, 'tool call name');
|
|
169
|
+
names.add(call.name);
|
|
170
|
+
if (call.isError === true) errors += 1;
|
|
171
|
+
}
|
|
172
|
+
const sorted = [...names].sort((left, right) => left.localeCompare(right));
|
|
173
|
+
return deepFreeze({
|
|
174
|
+
calls: toolCalls.length,
|
|
175
|
+
errors,
|
|
176
|
+
names: sorted.slice(0, MAX_RECEIPT_TOOL_NAMES),
|
|
177
|
+
excessNames: Math.max(0, sorted.length - MAX_RECEIPT_TOOL_NAMES),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function normalizeRelativeFilePath(value) {
|
|
182
|
+
if (typeof value !== 'string' || value.length === 0 || value.includes('\0')) return null;
|
|
183
|
+
const normalized = value.replaceAll('\\', '/');
|
|
184
|
+
if (normalized.startsWith('/') || /^[A-Za-z]:\//u.test(normalized) || normalized.startsWith('//')) return null;
|
|
185
|
+
const posix = path.posix.normalize(normalized);
|
|
186
|
+
if (posix === '.' || posix === '..' || posix.startsWith('../')) return null;
|
|
187
|
+
return posix;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function rootRelativeFile(absolutePath, root, rootRef, pathApi) {
|
|
191
|
+
const relative = pathApi.relative(pathApi.resolve(root), pathApi.resolve(absolutePath));
|
|
192
|
+
if (relative === '' || relative === '..' || relative.startsWith(`..${pathApi.sep}`) || pathApi.isAbsolute(relative)) return null;
|
|
193
|
+
const relativePath = normalizeRelativeFilePath(relative);
|
|
194
|
+
return relativePath === null ? null : {rootRef, relativePath};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function buildCronFileCandidate({toolName, toolCallId, display, workspaceRoot, additionalRoots = [], pathExists}) {
|
|
198
|
+
if (!['Write', 'Edit'].includes(toolName)) return null;
|
|
199
|
+
if (!display || display.kind !== 'file_io' || typeof display.path !== 'string') return null;
|
|
200
|
+
assertOpaqueId(toolCallId, 'toolCallId');
|
|
201
|
+
const pathApi = process.platform === 'win32' || /^[A-Za-z]:[\\/]/u.test(display.path) ? path.win32 : path;
|
|
202
|
+
let located = rootRelativeFile(display.path, workspaceRoot, 'root:workspace', pathApi);
|
|
203
|
+
if (located === null) {
|
|
204
|
+
for (let index = 0; index < additionalRoots.length; index += 1) {
|
|
205
|
+
located = rootRelativeFile(display.path, additionalRoots[index], `root:additional:${String(index + 1)}`, pathApi);
|
|
206
|
+
if (located !== null) break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (located === null) return null;
|
|
210
|
+
return deepFreeze({
|
|
211
|
+
...located,
|
|
212
|
+
change: toolName === 'Edit' || pathExists === true ? 'modified' : 'created',
|
|
213
|
+
toolCallId,
|
|
214
|
+
kind: 'file',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function normalizeCronOutputs(outputs) {
|
|
219
|
+
if (!Array.isArray(outputs)) fail('CRON_RUN_INVALID_OUTPUTS', 'outputs must be an array');
|
|
220
|
+
const safe = [];
|
|
221
|
+
for (const output of outputs) {
|
|
222
|
+
if (safe.length >= MAX_RECEIPT_OUTPUTS) break;
|
|
223
|
+
if (!output || output.kind !== 'file') continue;
|
|
224
|
+
if (!/^root:(workspace|additional:[1-9][0-9]*)$/u.test(output.rootRef)) continue;
|
|
225
|
+
const relativePath = normalizeRelativeFilePath(output.relativePath);
|
|
226
|
+
if (relativePath === null || !['created', 'modified'].includes(output.change)) continue;
|
|
227
|
+
safe.push({
|
|
228
|
+
rootRef: output.rootRef,
|
|
229
|
+
relativePath,
|
|
230
|
+
change: output.change,
|
|
231
|
+
toolCallId: assertOpaqueId(output.toolCallId, 'toolCallId'),
|
|
232
|
+
kind: 'file',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
return deepFreeze(safe);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function safeCronRunError(status, error) {
|
|
239
|
+
if (status !== 'failed') return undefined;
|
|
240
|
+
const category = error && ERROR_CATEGORIES.has(error.category) ? error.category : 'unknown';
|
|
241
|
+
const rawCode = error && typeof error.code === 'string' ? error.code : 'unspecified';
|
|
242
|
+
const code = /^[A-Za-z0-9._-]{1,64}$/u.test(rawCode) ? rawCode : 'redacted';
|
|
243
|
+
return {category, code};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function transitionCronRunReceipt(receipt, status, details = {}) {
|
|
247
|
+
const allowed = receipt && TRANSITIONS.get(receipt.status);
|
|
248
|
+
if (!allowed || !allowed.has(status)) {
|
|
249
|
+
fail('CRON_RUN_INVALID_TRANSITION', `Invalid cron run transition: ${String(receipt && receipt.status)} -> ${status}`);
|
|
250
|
+
}
|
|
251
|
+
if (status === 'running') {
|
|
252
|
+
if (!Number.isSafeInteger(details.turnId) || details.turnId < 0) {
|
|
253
|
+
fail('CRON_RUN_INVALID_TURN', 'turnId must be a non-negative integer');
|
|
254
|
+
}
|
|
255
|
+
return deepFreeze({
|
|
256
|
+
...clone(receipt),
|
|
257
|
+
status,
|
|
258
|
+
startedAt: isoTimestamp(details.startedAt, 'startedAt'),
|
|
259
|
+
turnId: details.turnId,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
const terminal = {
|
|
263
|
+
...clone(receipt),
|
|
264
|
+
status,
|
|
265
|
+
endedAt: isoTimestamp(details.endedAt, 'endedAt'),
|
|
266
|
+
toolSummary: summarizeCronToolCalls(details.toolCalls || []),
|
|
267
|
+
outputs: normalizeCronOutputs(details.outputs || []),
|
|
268
|
+
finalMessageRef: typeof details.finalStepId === 'string' && details.finalStepId.length > 0
|
|
269
|
+
? {stepId: assertOpaqueId(details.finalStepId, 'finalStepId')}
|
|
270
|
+
: null,
|
|
271
|
+
};
|
|
272
|
+
const error = safeCronRunError(status, details.error);
|
|
273
|
+
if (error !== undefined) terminal.error = error;
|
|
274
|
+
return deepFreeze(terminal);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function markCronJobDeleted(receipt) {
|
|
278
|
+
return deepFreeze({...clone(receipt), jobDeleted: true});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function isValidCronRunReceipt(value) {
|
|
282
|
+
if (!value || typeof value !== 'object') return false;
|
|
283
|
+
try {
|
|
284
|
+
assertOpaqueId(value.runId, 'runId');
|
|
285
|
+
assertOpaqueId(value.jobId, 'jobId');
|
|
286
|
+
isoTimestamp(value.firedAt, 'firedAt');
|
|
287
|
+
if (!ALL_STATUSES.has(value.status)) return false;
|
|
288
|
+
if (!Array.isArray(value.outputs) || !value.toolSummary || typeof value.toolSummary.calls !== 'number') return false;
|
|
289
|
+
if (value.status === 'running' && (!Number.isSafeInteger(value.turnId) || value.startedAt === null)) return false;
|
|
290
|
+
if (TERMINAL_STATUSES.has(value.status) && value.endedAt === null) return false;
|
|
291
|
+
return assertCronRunReceiptPrivacy(value);
|
|
292
|
+
} catch {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function publicCronRunList(receipts, {jobId, status, limit = 20} = {}) {
|
|
298
|
+
if (!Array.isArray(receipts)) fail('CRON_RUN_INVALID_LIST', 'receipts must be an array');
|
|
299
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
|
|
300
|
+
fail('CRON_RUN_INVALID_LIMIT', `limit must be between 1 and ${MAX_LIST_LIMIT}`);
|
|
301
|
+
}
|
|
302
|
+
if (status !== undefined && !ALL_STATUSES.has(status)) fail('CRON_RUN_INVALID_STATUS', 'Unknown cron run status');
|
|
303
|
+
return receipts
|
|
304
|
+
.filter((receipt) => jobId === undefined || receipt.jobId === jobId)
|
|
305
|
+
.filter((receipt) => status === undefined || receipt.status === status)
|
|
306
|
+
.sort((left, right) => Date.parse(right.firedAt) - Date.parse(left.firedAt))
|
|
307
|
+
.slice(0, limit)
|
|
308
|
+
.map((receipt) => ({
|
|
309
|
+
runId: receipt.runId,
|
|
310
|
+
jobId: receipt.jobId,
|
|
311
|
+
status: receipt.status,
|
|
312
|
+
firedAt: receipt.firedAt,
|
|
313
|
+
startedAt: receipt.startedAt,
|
|
314
|
+
endedAt: receipt.endedAt,
|
|
315
|
+
coalescedCount: receipt.coalescedCount,
|
|
316
|
+
toolCalls: receipt.toolSummary.calls,
|
|
317
|
+
toolErrors: receipt.toolSummary.errors,
|
|
318
|
+
outputCount: receipt.outputs.length,
|
|
319
|
+
finalMessageAvailable: receipt.finalMessageRef !== null,
|
|
320
|
+
}));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function assertCronRunReceiptPrivacy(receipt, canaries = []) {
|
|
324
|
+
const serialized = JSON.stringify(receipt);
|
|
325
|
+
const forbidden = ['thoughts', 'reasoning', 'prompt', 'arguments', 'results', 'headers', 'approvalFeedback', 'rawError'];
|
|
326
|
+
for (const key of forbidden) {
|
|
327
|
+
if (new RegExp(`"${key}"\\s*:`, 'u').test(serialized)) {
|
|
328
|
+
fail('CRON_RUN_PRIVATE_FIELD', `Forbidden cron receipt field: ${key}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
for (const canary of canaries) {
|
|
332
|
+
if (typeof canary === 'string' && canary.length > 0 && serialized.includes(canary)) {
|
|
333
|
+
fail('CRON_RUN_SECRET_LEAK', 'Cron receipt contains a secret canary');
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
module.exports = {
|
|
340
|
+
MAX_ALLOWED_TOOLS,
|
|
341
|
+
MAX_LIST_LIMIT,
|
|
342
|
+
MAX_RECEIPT_OUTPUTS,
|
|
343
|
+
ScopedCronRunError,
|
|
344
|
+
assertCronRunReceiptPrivacy,
|
|
345
|
+
buildCronFileCandidate,
|
|
346
|
+
createCronRunReceipt,
|
|
347
|
+
cronToolScopeDigest,
|
|
348
|
+
cronToolScopeForTask,
|
|
349
|
+
filterToolsForCronScope,
|
|
350
|
+
inheritedCronToolScope,
|
|
351
|
+
isCronToolScope,
|
|
352
|
+
isValidCronRunReceipt,
|
|
353
|
+
markCronJobDeleted,
|
|
354
|
+
normalizeCronAllowedTools,
|
|
355
|
+
publicCronRunList,
|
|
356
|
+
summarizeCronToolCalls,
|
|
357
|
+
transitionCronRunReceipt,
|
|
358
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const TOOL_ABORTED_BEFORE_DISPATCH = 'TOOL_ABORTED_BEFORE_DISPATCH';
|
|
4
|
+
|
|
5
|
+
async function checkpointBeforeExternalSideEffect(options = {}) {
|
|
6
|
+
const flush = options.flush;
|
|
7
|
+
const signal = options.signal;
|
|
8
|
+
if (typeof flush !== 'function') {
|
|
9
|
+
throw new TypeError('checkpointBeforeExternalSideEffect requires flush');
|
|
10
|
+
}
|
|
11
|
+
await flush();
|
|
12
|
+
if (signal?.aborted === true) {
|
|
13
|
+
return {
|
|
14
|
+
allowed: false,
|
|
15
|
+
code: TOOL_ABORTED_BEFORE_DISPATCH,
|
|
16
|
+
error: 'Tool call aborted before dispatch while its durable checkpoint was being written.',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
return { allowed: true };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
TOOL_ABORTED_BEFORE_DISPATCH,
|
|
24
|
+
checkpointBeforeExternalSideEffect,
|
|
25
|
+
};
|
|
@@ -20,7 +20,7 @@ function safeConfigPath(profileHome, fsImpl = fs) {
|
|
|
20
20
|
|
|
21
21
|
function parseStartupPreferences(text) {
|
|
22
22
|
let section = '';
|
|
23
|
-
let autoInstall =
|
|
23
|
+
let autoInstall = false;
|
|
24
24
|
let uiLocale = 'en';
|
|
25
25
|
|
|
26
26
|
for (const line of String(text).replace(/^\uFEFF/u, '').split(/\r?\n/u)) {
|
|
@@ -47,11 +47,11 @@ function parseStartupPreferences(text) {
|
|
|
47
47
|
|
|
48
48
|
function readStartupPreferences(profileHome, fsImpl = fs) {
|
|
49
49
|
const configPath = safeConfigPath(profileHome, fsImpl);
|
|
50
|
-
if (!configPath) return { autoInstall:
|
|
50
|
+
if (!configPath) return { autoInstall: false, uiLocale: 'en' };
|
|
51
51
|
try {
|
|
52
52
|
return parseStartupPreferences(fsImpl.readFileSync(configPath, 'utf8'));
|
|
53
53
|
} catch {
|
|
54
|
-
return { autoInstall:
|
|
54
|
+
return { autoInstall: false, uiLocale: 'en' };
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -125,6 +125,7 @@ function migrateLegacyStartupPreferences(profileHome, fsImpl = fs) {
|
|
|
125
125
|
|
|
126
126
|
module.exports = {
|
|
127
127
|
migrateLegacyStartupPreferences,
|
|
128
|
+
parseStartupPreferences,
|
|
128
129
|
readStartupPreferences,
|
|
129
130
|
resolveStartupUiLocale,
|
|
130
131
|
};
|