@jsonstudio/llms 0.6.954 → 0.6.1164

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 (130) hide show
  1. package/dist/conversion/hub/operation-table/operation-table-runner.d.ts +18 -0
  2. package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
  3. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
  4. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
  5. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
  6. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
  10. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
  11. package/dist/conversion/hub/ops/operations.d.ts +19 -0
  12. package/dist/conversion/hub/ops/operations.js +126 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
  14. package/dist/conversion/hub/pipeline/hub-pipeline.js +489 -19
  15. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
  16. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
  17. package/dist/conversion/hub/policy/policy-engine.js +41 -9
  18. package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
  19. package/dist/conversion/hub/policy/protocol-spec.js +73 -23
  20. package/dist/conversion/hub/process/chat-process.js +252 -41
  21. package/dist/conversion/hub/response/provider-response.js +175 -2
  22. package/dist/conversion/hub/response/response-runtime.js +1 -1
  23. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
  24. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
  25. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
  26. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -467
  27. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
  28. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -903
  29. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
  30. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
  31. package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
  32. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
  33. package/dist/conversion/responses/responses-openai-bridge.js +14 -2
  34. package/dist/conversion/shared/bridge-message-utils.js +2 -8
  35. package/dist/conversion/shared/bridge-policies.js +5 -105
  36. package/dist/conversion/shared/gemini-tool-utils.js +89 -15
  37. package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
  38. package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
  39. package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
  40. package/dist/conversion/shared/snapshot-hooks.js +166 -3
  41. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  42. package/dist/conversion/shared/text-markup-normalizer.js +345 -9
  43. package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
  44. package/dist/conversion/shared/thought-signature-validator.js +170 -0
  45. package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
  46. package/dist/conversion/shared/tool-argument-repairer.js +56 -0
  47. package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
  48. package/dist/conversion/shared/tool-call-id-manager.js +231 -0
  49. package/dist/conversion/shared/tool-canonicalizer.js +2 -11
  50. package/dist/router/virtual-router/bootstrap.js +54 -5
  51. package/dist/router/virtual-router/engine-selection.js +132 -42
  52. package/dist/router/virtual-router/engine.d.ts +3 -0
  53. package/dist/router/virtual-router/engine.js +142 -33
  54. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  55. package/dist/router/virtual-router/health-weighted.js +63 -0
  56. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  57. package/dist/router/virtual-router/load-balancer.js +45 -16
  58. package/dist/router/virtual-router/routing-instructions.js +17 -1
  59. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  60. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  61. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  62. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  63. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  64. package/dist/router/virtual-router/types.d.ts +70 -0
  65. package/dist/servertool/clock/config.d.ts +7 -0
  66. package/dist/servertool/clock/config.js +27 -0
  67. package/dist/servertool/clock/daemon.d.ts +3 -0
  68. package/dist/servertool/clock/daemon.js +79 -0
  69. package/dist/servertool/clock/io.d.ts +2 -0
  70. package/dist/servertool/clock/io.js +13 -0
  71. package/dist/servertool/clock/paths.d.ts +4 -0
  72. package/dist/servertool/clock/paths.js +25 -0
  73. package/dist/servertool/clock/session-store.d.ts +3 -0
  74. package/dist/servertool/clock/session-store.js +56 -0
  75. package/dist/servertool/clock/state.d.ts +5 -0
  76. package/dist/servertool/clock/state.js +62 -0
  77. package/dist/servertool/clock/task-store.d.ts +5 -0
  78. package/dist/servertool/clock/task-store.js +4 -0
  79. package/dist/servertool/clock/tasks.d.ts +17 -0
  80. package/dist/servertool/clock/tasks.js +221 -0
  81. package/dist/servertool/clock/types.d.ts +36 -0
  82. package/dist/servertool/clock/types.js +1 -0
  83. package/dist/servertool/engine.d.ts +2 -0
  84. package/dist/servertool/engine.js +161 -7
  85. package/dist/servertool/followup-shadow.d.ts +16 -0
  86. package/dist/servertool/followup-shadow.js +145 -0
  87. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  88. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  89. package/dist/servertool/handlers/clock-auto.js +160 -0
  90. package/dist/servertool/handlers/clock.d.ts +1 -0
  91. package/dist/servertool/handlers/clock.js +197 -0
  92. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  93. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  94. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  95. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  96. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  97. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  98. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  99. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  100. package/dist/servertool/handlers/vision.d.ts +7 -1
  101. package/dist/servertool/handlers/vision.js +61 -117
  102. package/dist/servertool/handlers/web-search.d.ts +7 -1
  103. package/dist/servertool/handlers/web-search.js +122 -105
  104. package/dist/servertool/reenter-backend.d.ts +23 -0
  105. package/dist/servertool/reenter-backend.js +18 -0
  106. package/dist/servertool/server-side-tools.d.ts +3 -2
  107. package/dist/servertool/server-side-tools.js +64 -10
  108. package/dist/servertool/types.d.ts +92 -3
  109. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  110. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  111. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  112. package/dist/sse/shared/writer.js +24 -7
  113. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  114. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  115. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  116. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  117. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  118. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  119. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  120. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  121. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  122. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  123. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  124. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  125. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  126. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  127. package/dist/tools/apply-patch/validation/shared.js +6 -0
  128. package/dist/tools/apply-patch/validator.d.ts +2 -2
  129. package/dist/tools/apply-patch/validator.js +6 -556
  130. package/package.json +1 -1
@@ -1,558 +1,10 @@
1
- import fs from 'node:fs/promises';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
1
  import { registerServerToolHandler } from '../registry.js';
5
- import { cloneJson } from '../server-side-tools.js';
6
- import { buildEntryAwareFollowupPayload, dropToolByFunctionName, extractCapturedChatSeed } from './followup-request-builder.js';
7
- const FLOW_ID = 'exec_command_guard';
8
- const BASELINE_RULES = [
9
- {
10
- id: 'baseline-rm-rf-root',
11
- reason: 'Destructive rm on filesystem root is not allowed',
12
- regex: /(^|\s)(sudo\s+)?rm\b[^\n]*(\s|^)(-rf|-fr|--recursive)[^\n]*\s(--\s*)?(\/\s*$|\/\*\s*$)/i
13
- },
14
- {
15
- id: 'baseline-rm-no-preserve-root',
16
- reason: 'rm with --no-preserve-root is not allowed',
17
- regex: /(^|\s)(sudo\s+)?rm\b[^\n]*--no-preserve-root\b/i
18
- },
19
- {
20
- id: 'baseline-find-delete-root',
21
- reason: 'find -delete on filesystem root is not allowed',
22
- regex: /(^|\s)find\s+\/(\s|$)[^\n]*\s-delete(\s|$)/i
23
- },
24
- {
25
- id: 'baseline-chmod-r-root',
26
- reason: 'chmod -R on filesystem root is not allowed',
27
- regex: /(^|\s)(sudo\s+)?chmod\b[^\n]*\s-R(\s|$)[^\n]*\s--?\s*\/(\s|$)/i
28
- },
29
- {
30
- id: 'baseline-chown-r-root',
31
- reason: 'chown -R on filesystem root is not allowed',
32
- regex: /(^|\s)(sudo\s+)?chown\b[^\n]*\s-R(\s|$)[^\n]*\s--?\s*\/(\s|$)/i
33
- },
34
- {
35
- id: 'baseline-disk-format',
36
- reason: 'Disk/partition formatting commands are not allowed',
37
- regex: /(^|\s)(sudo\s+)?(mkfs(\.|[a-z0-9_-]+)?|wipefs|fdisk|parted|sgdisk|shred)\b/i
38
- },
39
- {
40
- id: 'baseline-dd-to-dev',
41
- reason: 'dd writing to /dev is not allowed',
42
- regex: /(^|\s)(sudo\s+)?dd\b[^\n]*\sof=\/dev\/[^\s]+/i
43
- },
44
- {
45
- id: 'baseline-shutdown-reboot',
46
- reason: 'System shutdown/reboot commands are not allowed',
47
- regex: /(^|\s)(sudo\s+)?(shutdown|reboot|poweroff|halt|init)\b/i
48
- }
49
- ];
50
- const POLICY_CACHE = new Map();
51
- const POLICY_CACHE_TTL_MS = 3_000;
52
- const handler = async (ctx) => {
53
- const toolCall = ctx.toolCall;
54
- if (!toolCall) {
55
- return null;
56
- }
57
- if (!ctx.options.reenterPipeline) {
58
- return null;
59
- }
60
- const guardConfig = getExecCommandGuardConfig(ctx.adapterContext);
61
- if (!guardConfig.enabled) {
62
- return null;
63
- }
64
- const parsedArgs = parseToolArguments(toolCall);
65
- const cmd = typeof parsedArgs.cmd === 'string' && parsedArgs.cmd.trim() ? parsedArgs.cmd.trim() : '';
66
- const workdir = typeof parsedArgs.workdir === 'string' && parsedArgs.workdir.trim() ? parsedArgs.workdir.trim() : undefined;
67
- if (!cmd) {
68
- return null;
69
- }
70
- const decision = await evaluateExecCommandDeny({
71
- cmd,
72
- workdir,
73
- policyFile: guardConfig.policyFile
74
- });
75
- if (!decision.blocked) {
76
- return null;
77
- }
78
- const patched = injectExecCommandDeniedToolResult(ctx.base, toolCall, {
79
- cmd,
80
- workdir,
81
- decision
82
- });
83
- const followupPayload = buildToolFollowupPayload(ctx.adapterContext, patched, ctx.options.entryEndpoint || ctx.adapterContext?.entryEndpoint || '/v1/chat/completions', {
84
- dropToolName: 'exec_command'
85
- });
86
- if (!followupPayload) {
87
- // Fail-closed: if we cannot perform reenter, do not intercept.
88
- // (This should be rare because HubPipeline always provides capturedChatRequest.)
89
- return null;
90
- }
91
- return {
92
- chatResponse: patched,
93
- execution: {
94
- flowId: FLOW_ID,
95
- followup: {
96
- requestIdSuffix: ':exec_command_guard_followup',
97
- payload: followupPayload
98
- }
99
- }
100
- };
2
+ const handler = async (_ctx) => {
3
+ // exec_command is executed by the client. ServerTool must not fabricate tool outputs
4
+ // or followups; client will run (or fail) and send the real tool_result in next request.
5
+ //
6
+ // Shape-only normalization is handled by tool-governor (validateToolCall + normalizedArgs)
7
+ // before the client receives the tool call.
8
+ return null;
101
9
  };
102
10
  registerServerToolHandler('exec_command', handler);
103
- function getExecCommandGuardConfig(adapterContext) {
104
- const raw = adapterContext && typeof adapterContext === 'object'
105
- ? adapterContext.execCommandGuard
106
- : undefined;
107
- if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
108
- return { enabled: false };
109
- }
110
- const cfg = raw;
111
- const enabledRaw = cfg.enabled;
112
- const enabled = enabledRaw === true ||
113
- (typeof enabledRaw === 'string' && enabledRaw.trim().toLowerCase() === 'true') ||
114
- (typeof enabledRaw === 'number' && enabledRaw === 1);
115
- if (!enabled) {
116
- return { enabled: false };
117
- }
118
- const policyFileRaw = cfg.policyFile;
119
- const policyFile = typeof policyFileRaw === 'string' && policyFileRaw.trim() ? policyFileRaw.trim() : undefined;
120
- return { enabled: true, ...(policyFile ? { policyFile } : {}) };
121
- }
122
- function parseToolArguments(toolCall) {
123
- if (!toolCall.arguments || typeof toolCall.arguments !== 'string') {
124
- return {};
125
- }
126
- try {
127
- return JSON.parse(toolCall.arguments);
128
- }
129
- catch {
130
- return {};
131
- }
132
- }
133
- async function evaluateExecCommandDeny(options) {
134
- const baseline = matchRegexRules(options.cmd, BASELINE_RULES);
135
- if (baseline) {
136
- return { blocked: true, ruleId: baseline.id, reason: baseline.reason, source: 'baseline', policyFile: options.policyFile };
137
- }
138
- const policyFile = typeof options.policyFile === 'string' && options.policyFile.trim() ? options.policyFile.trim() : '';
139
- if (!policyFile) {
140
- return { blocked: false };
141
- }
142
- const loaded = await loadPolicy(policyFile);
143
- if (!loaded.policy) {
144
- return {
145
- blocked: false,
146
- policyFile,
147
- ...(loaded.error ? { policyNote: loaded.error } : {})
148
- };
149
- }
150
- const combined = `${options.cmd}\nworkdir=${options.workdir ?? ''}`;
151
- // Defaults: structured enforcement for common safety invariants (policy-controlled).
152
- if (loaded.policy.denyOutsideProjectDestructive) {
153
- const out = evaluateOutsideProjectDestructive({
154
- cmd: options.cmd,
155
- workdir: options.workdir,
156
- allowedProjectRoots: loaded.policy.allowedProjectRoots
157
- });
158
- if (out) {
159
- return {
160
- blocked: true,
161
- ruleId: out.id,
162
- reason: out.reason,
163
- source: 'policy-defaults',
164
- policyFile
165
- };
166
- }
167
- }
168
- if (loaded.policy.gitSingleFileOnly) {
169
- const out = evaluateGitSingleFileOnly(options.cmd);
170
- if (out) {
171
- return {
172
- blocked: true,
173
- ruleId: out.id,
174
- reason: out.reason,
175
- source: 'policy-defaults',
176
- policyFile
177
- };
178
- }
179
- }
180
- if (loaded.policy.denyMassKill) {
181
- const out = evaluateMassKill(options.cmd);
182
- if (out) {
183
- return {
184
- blocked: true,
185
- ruleId: out.id,
186
- reason: out.reason,
187
- source: 'policy-defaults',
188
- policyFile
189
- };
190
- }
191
- }
192
- for (const rule of loaded.policy.rules) {
193
- const targetText = rule.target === 'cmd+workdir' ? combined : options.cmd;
194
- if (rule.regex.test(targetText)) {
195
- return {
196
- blocked: true,
197
- ruleId: rule.id,
198
- reason: rule.reason,
199
- source: 'policy',
200
- policyFile
201
- };
202
- }
203
- }
204
- return { blocked: false };
205
- }
206
- function matchRegexRules(cmd, rules) {
207
- for (const rule of rules) {
208
- try {
209
- if (rule.regex.test(cmd)) {
210
- return { id: rule.id, reason: rule.reason };
211
- }
212
- }
213
- catch {
214
- // ignore invalid regex evaluation
215
- }
216
- }
217
- return null;
218
- }
219
- function injectExecCommandDeniedToolResult(base, toolCall, options) {
220
- const cloned = cloneJson(base);
221
- const existingOutputs = Array.isArray(cloned.tool_outputs)
222
- ? cloned.tool_outputs
223
- : [];
224
- const payload = {
225
- ok: false,
226
- blocked: true,
227
- tool: 'exec_command',
228
- cmd: options.cmd,
229
- ...(options.workdir ? { workdir: options.workdir } : {}),
230
- ...(options.decision.ruleId ? { ruleId: options.decision.ruleId } : {}),
231
- ...(options.decision.reason ? { reason: options.decision.reason } : {}),
232
- ...(options.decision.source ? { source: options.decision.source } : {}),
233
- ...(options.decision.policyFile ? { policyFile: options.decision.policyFile } : {}),
234
- ...(options.decision.policyNote ? { policyNote: options.decision.policyNote } : {}),
235
- guidance: 'Command execution is blocked by server policy. If you believe this is safe, adjust the exec_command deny policy file.'
236
- };
237
- cloned.tool_outputs = [
238
- ...existingOutputs,
239
- {
240
- tool_call_id: toolCall.id,
241
- name: 'exec_command',
242
- content: JSON.stringify(payload)
243
- }
244
- ];
245
- return cloned;
246
- }
247
- function buildToolFollowupPayload(adapterContext, chatResponse, entryEndpoint, options) {
248
- const captured = adapterContext && typeof adapterContext === 'object'
249
- ? adapterContext.capturedChatRequest
250
- : undefined;
251
- const seed = extractCapturedChatSeed(captured);
252
- if (!seed) {
253
- return null;
254
- }
255
- const assistantMessage = extractAssistantMessage(chatResponse);
256
- if (!assistantMessage) {
257
- return null;
258
- }
259
- const toolMessages = buildToolMessages(chatResponse);
260
- if (!toolMessages.length) {
261
- return null;
262
- }
263
- const reconstructed = [...seed.messages, assistantMessage, ...toolMessages];
264
- const dropName = typeof options.dropToolName === 'string' ? options.dropToolName.trim() : '';
265
- const filteredTools = dropToolByFunctionName(seed.tools, dropName);
266
- return buildEntryAwareFollowupPayload({
267
- entryEndpoint,
268
- model: seed.model,
269
- messages: reconstructed,
270
- ...(filteredTools ? { tools: filteredTools } : {}),
271
- ...(seed.parameters ? { parameters: seed.parameters } : {})
272
- });
273
- }
274
- function extractAssistantMessage(chatResponse) {
275
- const choices = Array.isArray(chatResponse.choices)
276
- ? chatResponse.choices
277
- : [];
278
- if (!choices.length)
279
- return null;
280
- const firstChoice = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0])
281
- ? choices[0]
282
- : null;
283
- if (!firstChoice)
284
- return null;
285
- const assistantMessage = firstChoice.message && typeof firstChoice.message === 'object'
286
- ? firstChoice.message
287
- : null;
288
- return assistantMessage;
289
- }
290
- function buildToolMessages(chatResponse) {
291
- const toolOutputs = Array.isArray(chatResponse.tool_outputs)
292
- ? chatResponse.tool_outputs
293
- : [];
294
- const messages = [];
295
- for (const entry of toolOutputs) {
296
- if (!entry || typeof entry !== 'object' || Array.isArray(entry))
297
- continue;
298
- const record = entry;
299
- const toolCallId = typeof record.tool_call_id === 'string' ? record.tool_call_id : undefined;
300
- if (!toolCallId)
301
- continue;
302
- const name = typeof record.name === 'string' && record.name.trim() ? record.name.trim() : 'exec_command';
303
- const rawContent = record.content;
304
- let contentText;
305
- if (typeof rawContent === 'string') {
306
- contentText = rawContent;
307
- }
308
- else {
309
- try {
310
- contentText = JSON.stringify(rawContent ?? {});
311
- }
312
- catch {
313
- contentText = String(rawContent ?? '');
314
- }
315
- }
316
- messages.push({
317
- role: 'tool',
318
- tool_call_id: toolCallId,
319
- name,
320
- content: contentText
321
- });
322
- }
323
- return messages;
324
- }
325
- function expandUserPath(inputPath) {
326
- const p = inputPath.trim();
327
- if (!p)
328
- return p;
329
- if (p === '~')
330
- return os.homedir();
331
- if (p.startsWith('~/') || p.startsWith('~\\')) {
332
- return path.join(os.homedir(), p.slice(2));
333
- }
334
- return p;
335
- }
336
- async function loadPolicy(policyFile) {
337
- const resolved = path.resolve(expandUserPath(policyFile));
338
- const now = Date.now();
339
- const cached = POLICY_CACHE.get(resolved);
340
- if (cached && now - cached.loadedAt <= POLICY_CACHE_TTL_MS) {
341
- if (cached.inFlight) {
342
- await cached.inFlight;
343
- }
344
- return { policy: cached.policy, error: cached.error };
345
- }
346
- const state = cached ?? { loadedAt: 0 };
347
- let inFlightResolve;
348
- const inFlight = new Promise((resolve) => {
349
- inFlightResolve = resolve;
350
- });
351
- POLICY_CACHE.set(resolved, { ...state, loadedAt: now, inFlight });
352
- try {
353
- const st = await fs.stat(resolved);
354
- const mtimeMs = typeof st.mtimeMs === 'number' ? st.mtimeMs : undefined;
355
- if (cached && cached.policy && typeof cached.mtimeMs === 'number' && typeof mtimeMs === 'number' && cached.mtimeMs === mtimeMs) {
356
- POLICY_CACHE.set(resolved, { loadedAt: now, mtimeMs, policy: cached.policy });
357
- return { policy: cached.policy };
358
- }
359
- const raw = await fs.readFile(resolved, 'utf8');
360
- const parsed = raw.trim() ? JSON.parse(raw) : {};
361
- const policy = parsePolicy(parsed, resolved);
362
- POLICY_CACHE.set(resolved, { loadedAt: now, mtimeMs, policy });
363
- return { policy };
364
- }
365
- catch (error) {
366
- const message = error instanceof Error ? error.message : String(error ?? 'unknown');
367
- POLICY_CACHE.set(resolved, { loadedAt: now, error: `[exec_command_guard] policy load failed: ${message}` });
368
- return { error: `[exec_command_guard] policy load failed: ${message}` };
369
- }
370
- finally {
371
- try {
372
- inFlightResolve?.();
373
- }
374
- catch {
375
- /* ignore */
376
- }
377
- }
378
- }
379
- function parsePolicy(raw, policyFile) {
380
- const version = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw.version : undefined;
381
- if (version !== 1) {
382
- return {
383
- version: 1,
384
- policyFile,
385
- allowedProjectRoots: [],
386
- denyOutsideProjectDestructive: false,
387
- gitSingleFileOnly: false,
388
- denyMassKill: false,
389
- rules: []
390
- };
391
- }
392
- const allowedProjectRoots = [];
393
- const rootsRaw = raw.allowedProjectRoots;
394
- if (Array.isArray(rootsRaw)) {
395
- for (const entry of rootsRaw) {
396
- if (typeof entry === 'string' && entry.trim()) {
397
- allowedProjectRoots.push(path.resolve(expandUserPath(entry.trim())));
398
- }
399
- }
400
- }
401
- const defaultsRaw = raw.defaults;
402
- const defaults = defaultsRaw && typeof defaultsRaw === 'object' && !Array.isArray(defaultsRaw)
403
- ? defaultsRaw
404
- : {};
405
- const denyOutsideProjectDestructive = defaults.denyOutsideProjectDestructive === true ||
406
- (typeof defaults.denyOutsideProjectDestructive === 'string' &&
407
- String(defaults.denyOutsideProjectDestructive).trim().toLowerCase() === 'true');
408
- const gitSingleFileOnly = defaults.gitSingleFileOnly === true ||
409
- (typeof defaults.gitSingleFileOnly === 'string' && String(defaults.gitSingleFileOnly).trim().toLowerCase() === 'true');
410
- const denyMassKill = defaults.denyMassKill === true ||
411
- (typeof defaults.denyMassKill === 'string' && String(defaults.denyMassKill).trim().toLowerCase() === 'true');
412
- const rules = [];
413
- const rulesRaw = raw.rules;
414
- if (Array.isArray(rulesRaw)) {
415
- for (const entry of rulesRaw) {
416
- if (!entry || typeof entry !== 'object' || Array.isArray(entry))
417
- continue;
418
- const rule = entry;
419
- const type = typeof rule.type === 'string' ? rule.type.trim().toLowerCase() : 'regex';
420
- if (type !== 'regex')
421
- continue;
422
- const id = typeof rule.id === 'string' && rule.id.trim() ? rule.id.trim() : '';
423
- const pattern = typeof rule.pattern === 'string' && rule.pattern.trim() ? rule.pattern.trim() : '';
424
- if (!id || !pattern)
425
- continue;
426
- const flags = typeof rule.flags === 'string' ? rule.flags.trim() : 'i';
427
- const targetRaw = typeof rule.target === 'string' ? rule.target.trim().toLowerCase() : 'cmd';
428
- const target = targetRaw === 'cmd+workdir' ? 'cmd+workdir' : 'cmd';
429
- let regex;
430
- try {
431
- regex = new RegExp(pattern, flags);
432
- }
433
- catch {
434
- continue;
435
- }
436
- const reason = typeof rule.reason === 'string' && rule.reason.trim() ? rule.reason.trim() : undefined;
437
- rules.push({ id, reason, target, regex });
438
- }
439
- }
440
- return {
441
- version: 1,
442
- policyFile,
443
- allowedProjectRoots,
444
- denyOutsideProjectDestructive,
445
- gitSingleFileOnly,
446
- denyMassKill,
447
- rules
448
- };
449
- }
450
- function isSubPath(child, parent) {
451
- const rel = path.relative(parent, child);
452
- if (!rel)
453
- return true;
454
- return !rel.startsWith('..' + path.sep) && rel !== '..';
455
- }
456
- function evaluateOutsideProjectDestructive(options) {
457
- const workdir = typeof options.workdir === 'string' && options.workdir.trim() ? path.resolve(expandUserPath(options.workdir.trim())) : '';
458
- const roots = options.allowedProjectRoots;
459
- if (!workdir || !roots.length) {
460
- return null;
461
- }
462
- const inProject = roots.some((root) => isSubPath(workdir, root));
463
- if (inProject) {
464
- return null;
465
- }
466
- // Only enforce for clearly destructive operations (policy default).
467
- const destructive = /(^|\s)(sudo\s+)?rm\b[^\n]*(\s|^)(-rf|-fr|-r|-f|--recursive)(\s|$)/i.test(options.cmd) ||
468
- /(^|\s)find\b[^\n]*\s-delete(\s|$)/i.test(options.cmd);
469
- if (!destructive) {
470
- return null;
471
- }
472
- // If user is not in a project dir, deny destructive commands that reference absolute paths or home paths.
473
- const targetsAbsoluteOrHome = /(^|\s)(\/|~\/)/.test(options.cmd) ||
474
- /\s(\/|~\/)/.test(options.cmd);
475
- if (!targetsAbsoluteOrHome) {
476
- return null;
477
- }
478
- return {
479
- id: 'policy-defaults-deny-outside-project-destructive',
480
- reason: 'Destructive command outside allowed project roots is not allowed'
481
- };
482
- }
483
- function evaluateGitSingleFileOnly(cmd) {
484
- const trimmed = cmd.trim();
485
- if (!/^git\s+(checkout|restore)\b/i.test(trimmed)) {
486
- return null;
487
- }
488
- // Only enforce when pathspec is provided explicitly with "--" (safer parsing).
489
- const idx = trimmed.indexOf(' -- ');
490
- if (idx < 0) {
491
- return {
492
- id: 'policy-defaults-git-single-file-only',
493
- reason: 'git checkout/restore is restricted to single-file pathspec using `-- <file>`'
494
- };
495
- }
496
- const pathspec = trimmed.slice(idx + 4).trim();
497
- if (!pathspec) {
498
- return {
499
- id: 'policy-defaults-git-single-file-only',
500
- reason: 'git checkout/restore requires a single file pathspec'
501
- };
502
- }
503
- if (pathspec === '.' || pathspec === './' || pathspec === '..' || pathspec === '../') {
504
- return {
505
- id: 'policy-defaults-git-single-file-only',
506
- reason: 'Directory pathspec is not allowed for git checkout/restore'
507
- };
508
- }
509
- if (/\s/.test(pathspec)) {
510
- return {
511
- id: 'policy-defaults-git-single-file-only',
512
- reason: 'Multiple pathspecs are not allowed for git checkout/restore'
513
- };
514
- }
515
- if (pathspec.endsWith('/') || pathspec.includes('*') || pathspec.includes('?')) {
516
- return {
517
- id: 'policy-defaults-git-single-file-only',
518
- reason: 'Directory/glob pathspec is not allowed for git checkout/restore'
519
- };
520
- }
521
- return null;
522
- }
523
- function evaluateMassKill(cmd) {
524
- const trimmed = cmd.trim();
525
- if (/^pkill\b/i.test(trimmed) || /^killall\b/i.test(trimmed)) {
526
- return {
527
- id: 'policy-defaults-deny-mass-kill',
528
- reason: 'pkill/killall are not allowed'
529
- };
530
- }
531
- if (!/^kill\b/i.test(trimmed)) {
532
- return null;
533
- }
534
- // Allow kill by PID(s) only; deny name-based / pattern-based killing.
535
- const parts = trimmed.split(/\s+/).slice(1);
536
- if (!parts.length) {
537
- return {
538
- id: 'policy-defaults-deny-mass-kill',
539
- reason: 'kill must target a specific PID'
540
- };
541
- }
542
- const targets = parts.filter((p) => !p.startsWith('-'));
543
- if (!targets.length) {
544
- return {
545
- id: 'policy-defaults-deny-mass-kill',
546
- reason: 'kill must target a specific PID'
547
- };
548
- }
549
- for (const t of targets) {
550
- if (!/^[0-9]+$/.test(t)) {
551
- return {
552
- id: 'policy-defaults-deny-mass-kill',
553
- reason: 'kill is restricted to PID-only targets'
554
- };
555
- }
556
- }
557
- return null;
558
- }
@@ -1,4 +1,5 @@
1
1
  import type { JsonObject } from '../../conversion/hub/types/json.js';
2
+ import type { ServerToolFollowupInjectionPlan } from '../types.js';
2
3
  export type CapturedChatSeed = {
3
4
  model?: string;
4
5
  messages: JsonObject[];
@@ -7,11 +8,18 @@ export type CapturedChatSeed = {
7
8
  };
8
9
  export declare function extractCapturedChatSeed(source: unknown): CapturedChatSeed | null;
9
10
  export declare function normalizeFollowupParameters(value: unknown): Record<string, unknown> | undefined;
10
- export declare function buildEntryAwareFollowupPayload(args: {
11
- entryEndpoint: string;
12
- model?: string;
13
- messages: JsonObject[];
14
- tools?: JsonObject[];
15
- parameters?: Record<string, unknown>;
16
- }): JsonObject;
17
11
  export declare function dropToolByFunctionName(tools: JsonObject[] | undefined, dropName: string): JsonObject[] | undefined;
12
+ /**
13
+ * Build a canonical followup request body from injection ops.
14
+ *
15
+ * Important: this returns a protocol-agnostic "chat-like" payload:
16
+ * { model, messages, tools?, parameters? }
17
+ *
18
+ * The followup is expected to re-enter HubPipeline at the chat-process entry,
19
+ * so we must not convert to /v1/responses or /v1/messages here.
20
+ */
21
+ export declare function buildServerToolFollowupChatPayloadFromInjection(args: {
22
+ adapterContext: unknown;
23
+ chatResponse: JsonObject;
24
+ injection: ServerToolFollowupInjectionPlan;
25
+ }): JsonObject | null;