@jsonstudio/llms 0.6.795 → 0.6.938
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/dist/bridge/routecodex-adapter.d.ts +74 -0
- package/dist/config-unified/enhanced-path-resolver.d.ts +5 -0
- package/dist/config-unified/unified-config.d.ts +26 -0
- package/dist/conversion/codec-registry.d.ts +10 -0
- package/dist/conversion/codecs/gemini-openai-codec.d.ts +16 -0
- package/dist/conversion/codecs/openai-openai-codec.d.ts +12 -0
- package/dist/conversion/codecs/responses-openai-codec.d.ts +12 -0
- package/dist/conversion/compat/profiles/chat-gemini.json +12 -0
- package/dist/conversion/config/config-manager.d.ts +212 -0
- package/dist/conversion/hub/config/types.d.ts +26 -0
- package/dist/conversion/hub/core/detour-registry.d.ts +9 -0
- package/dist/conversion/hub/core/hub-context.d.ts +21 -0
- package/dist/conversion/hub/core/index.d.ts +3 -0
- package/dist/conversion/hub/core/stage-driver.d.ts +30 -0
- package/dist/conversion/hub/format-adapters/anthropic-format-adapter.d.ts +16 -0
- package/dist/conversion/hub/format-adapters/chat-format-adapter.d.ts +17 -0
- package/dist/conversion/hub/format-adapters/gemini-format-adapter.d.ts +16 -0
- package/dist/conversion/hub/format-adapters/index.d.ts +21 -0
- package/dist/conversion/hub/hub-feature.d.ts +1 -0
- package/dist/conversion/hub/node-support.d.ts +19 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +11 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +7 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +71 -14
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +4 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +23 -1
- package/dist/conversion/hub/pipelines/inbound.d.ts +22 -0
- package/dist/conversion/hub/pipelines/outbound.d.ts +22 -0
- package/dist/conversion/hub/policy/policy-engine.d.ts +46 -0
- package/dist/conversion/hub/policy/policy-engine.js +176 -0
- package/dist/conversion/hub/policy/protocol-spec.d.ts +50 -0
- package/dist/conversion/hub/policy/protocol-spec.js +105 -0
- package/dist/conversion/hub/process/chat-process.d.ts +32 -0
- package/dist/conversion/hub/registry.d.ts +28 -0
- package/dist/conversion/hub/response/chat-response-utils.d.ts +6 -0
- package/dist/conversion/hub/response/provider-response.js +31 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +7 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +87 -1
- package/dist/conversion/hub/semantic-mappers/index.d.ts +4 -0
- package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +21 -0
- package/dist/conversion/hub/standardized-bridge.d.ts +12 -0
- package/dist/conversion/hub/types/chat-schema.d.ts +112 -0
- package/dist/conversion/hub/types/errors.d.ts +5 -0
- package/dist/conversion/hub/types/format-envelope.d.ts +7 -0
- package/dist/conversion/hub/types/index.d.ts +6 -0
- package/dist/conversion/hub/types/json.d.ts +9 -0
- package/dist/conversion/hub/types/node.d.ts +31 -0
- package/dist/conversion/responses/responses-openai-bridge.js +263 -10
- package/dist/conversion/schema-validator.d.ts +7 -0
- package/dist/conversion/shared/args-mapping.d.ts +18 -0
- package/dist/conversion/shared/chat-request-filters.d.ts +9 -0
- package/dist/conversion/shared/errors.d.ts +1 -1
- package/dist/conversion/shared/gemini-tool-utils.js +61 -0
- package/dist/conversion/shared/jsonish.d.ts +3 -0
- package/dist/conversion/shared/mcp-injection.d.ts +2 -0
- package/dist/conversion/shared/media.d.ts +1 -0
- package/dist/conversion/shared/openai-message-normalize.d.ts +1 -0
- package/dist/conversion/shared/payload-budget.d.ts +13 -0
- package/dist/conversion/shared/reasoning-mapping.d.ts +5 -0
- package/dist/conversion/shared/responses-request-adapter.d.ts +1 -28
- package/dist/conversion/shared/responses-request-adapter.js +1 -430
- package/dist/conversion/shared/snapshot-hooks.js +112 -4
- package/dist/conversion/shared/tool-governor.js +8 -2
- package/dist/conversion/shared/tool-harvester.d.ts +31 -0
- package/dist/conversion/shared/tool-mapping.js +10 -29
- package/dist/conversion/types.d.ts +33 -0
- package/dist/filters/builtin/add-fields-filter.d.ts +8 -0
- package/dist/filters/builtin/blacklist-filter.d.ts +8 -0
- package/dist/filters/builtin/whitelist-filter.d.ts +8 -0
- package/dist/filters/engine.d.ts +16 -0
- package/dist/filters/special/request-tool-choice-policy.d.ts +11 -0
- package/dist/filters/special/response-finish-invariants.d.ts +11 -0
- package/dist/filters/special/response-openai-to-responses-bridge.d.ts +13 -0
- package/dist/filters/special/response-tool-arguments-blacklist.d.ts +12 -0
- package/dist/filters/special/response-tool-arguments-schema-converge.d.ts +13 -0
- package/dist/filters/special/response-tool-arguments-stringify.d.ts +9 -0
- package/dist/filters/special/response-tool-arguments-whitelist.d.ts +11 -0
- package/dist/filters/special/tool-filter-hooks.d.ts +19 -0
- package/dist/filters/special/tool-post-constraints.d.ts +31 -0
- package/dist/filters/types.d.ts +68 -0
- package/dist/filters/utils/fieldmap-loader.d.ts +2 -0
- package/dist/filters/utils/snapshot-writer.d.ts +10 -0
- package/dist/guidance/index.d.ts +3 -0
- package/dist/guidance/index.js +78 -83
- package/dist/http/sse-response.d.ts +22 -0
- package/dist/router/virtual-router/bootstrap.d.ts +6 -0
- package/dist/router/virtual-router/bootstrap.js +49 -5
- package/dist/router/virtual-router/classifier.d.ts +10 -0
- package/dist/router/virtual-router/engine-selection.js +147 -15
- package/dist/router/virtual-router/engine.js +177 -31
- package/dist/router/virtual-router/error-center.d.ts +10 -0
- package/dist/router/virtual-router/features.d.ts +3 -0
- package/dist/router/virtual-router/routing-instructions.d.ts +23 -1
- package/dist/router/virtual-router/routing-instructions.js +120 -30
- package/dist/router/virtual-router/types.d.ts +11 -0
- package/dist/servertool/engine.js +189 -16
- package/dist/servertool/handlers/apply-patch-guard.js +269 -0
- package/dist/servertool/handlers/exec-command-guard.js +558 -0
- package/dist/servertool/handlers/followup-message-trimmer.d.ts +16 -0
- package/dist/servertool/handlers/followup-message-trimmer.js +198 -0
- package/dist/servertool/handlers/followup-request-builder.d.ts +17 -0
- package/dist/servertool/handlers/followup-request-builder.js +122 -0
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +252 -51
- package/dist/servertool/handlers/iflow-model-error-retry.js +12 -22
- package/dist/servertool/handlers/stop-message-auto.js +237 -75
- package/dist/servertool/handlers/vision.js +15 -27
- package/dist/servertool/handlers/web-search.js +17 -43
- package/dist/servertool/server-side-tools.d.ts +3 -0
- package/dist/servertool/server-side-tools.js +3 -0
- package/dist/sse/json-to-sse/anthropic-json-to-sse-converter.d.ts +2 -1
- package/dist/sse/json-to-sse/chat-json-to-sse-converter.d.ts +80 -0
- package/dist/sse/json-to-sse/event-generators/chat.d.ts +55 -0
- package/dist/sse/json-to-sse/event-generators/responses.d.ts +99 -0
- package/dist/sse/json-to-sse/gemini-json-to-sse-converter.d.ts +2 -1
- package/dist/sse/json-to-sse/responses-json-to-sse-converter.d.ts +80 -0
- package/dist/sse/json-to-sse/sequencers/anthropic-sequencer.d.ts +1 -1
- package/dist/sse/json-to-sse/sequencers/chat-sequencer.d.ts +2 -2
- package/dist/sse/json-to-sse/sequencers/gemini-sequencer.d.ts +1 -1
- package/dist/sse/json-to-sse/sequencers/responses-sequencer.d.ts +40 -0
- package/dist/sse/shared/chat-serializer.d.ts +4 -0
- package/dist/sse/shared/constants.d.ts +272 -0
- package/dist/sse/shared/serializers/anthropic-event-serializer.d.ts +1 -1
- package/dist/sse/shared/serializers/base-serializer.d.ts +158 -0
- package/dist/sse/shared/serializers/chat-event-serializer.d.ts +82 -0
- package/dist/sse/shared/serializers/gemini-event-serializer.d.ts +1 -1
- package/dist/sse/shared/serializers/index.d.ts +2 -1
- package/dist/sse/shared/serializers/responses-event-serializer.d.ts +123 -0
- package/dist/sse/shared/serializers/types.d.ts +51 -0
- package/dist/sse/shared/utils.d.ts +254 -0
- package/dist/sse/shared/writer.d.ts +2 -2
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -1
- package/dist/sse/sse-to-json/builders/anthropic-response-builder.d.ts +1 -1
- package/dist/sse/sse-to-json/builders/response-builder.d.ts +1 -1
- package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +2 -1
- package/dist/sse/sse-to-json/gemini-sse-to-json-converter.d.ts +2 -1
- package/dist/sse/sse-to-json/parsers/sse-parser.d.ts +73 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
- package/dist/sse/types/chat-types.d.ts +1 -1
- package/dist/sse/types/responses-types.d.ts +1 -1
- package/dist/tools/apply-patch/execution-capturer.d.ts +13 -0
- package/dist/tools/apply-patch/execution-capturer.js +158 -0
- package/dist/tools/apply-patch/regression-capturer.d.ts +1 -0
- package/dist/tools/apply-patch/regression-capturer.js +5 -4
- package/dist/tools/apply-patch/structured.js +109 -13
- package/dist/tools/apply-patch/validator.js +261 -17
- package/dist/tools/tool-registry.d.ts +8 -0
- package/dist/tools/tool-registry.js +2 -1
- package/package.json +4 -4
- package/dist/conversion/compat/actions/apply-patch-format-fixer.js +0 -233
- package/dist/conversion/config/compat-profiles.json +0 -38
- package/dist/conversion/hub/response/server-side-tools.d.ts +0 -26
- package/dist/conversion/hub/response/server-side-tools.js +0 -383
- package/dist/conversion/shared/bridge-conversation-store.d.ts +0 -41
- package/dist/conversion/shared/bridge-conversation-store.js +0 -279
- package/dist/conversion/shared/bridge-request-adapter.d.ts +0 -28
- package/dist/conversion/shared/bridge-request-adapter.js +0 -430
- package/dist/conversion/shared/responses-id-utils.js +0 -42
- package/dist/conversion/shared/responses-instructions.js +0 -113
- package/dist/conversion/shared/responses-message-utils.d.ts +0 -15
- package/dist/conversion/shared/responses-message-utils.js +0 -206
- package/dist/conversion/shared/responses-metadata.js +0 -1
- package/dist/conversion/shared/responses-output-utils.d.ts +0 -7
- package/dist/conversion/shared/responses-output-utils.js +0 -108
- package/dist/conversion/shared/responses-types.d.ts +0 -33
- package/dist/conversion/shared/tool-normalizers.d.ts +0 -4
- package/dist/conversion/shared/tool-normalizers.js +0 -84
- package/dist/filters/special/request-streaming-to-nonstreaming.d.ts +0 -13
- package/dist/filters/special/request-streaming-to-nonstreaming.js +0 -39
- package/dist/filters/special/response-apply-patch-toon-decode.d.ts +0 -23
- package/dist/filters/special/response-apply-patch-toon-decode.js +0 -460
- package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +0 -10
- package/dist/filters/special/response-tool-arguments-toon-decode.js +0 -154
- package/dist/servertool/flow-types.d.ts +0 -40
- package/dist/servertool/flow-types.js +0 -1
- package/dist/servertool/orchestration-types.d.ts +0 -33
- package/dist/servertool/orchestration-types.js +0 -1
- package/dist/servertool/vision-tool.d.ts +0 -2
- package/dist/servertool/vision-tool.js +0 -185
- package/dist/tools/patch-args-normalizer.d.ts +0 -15
- package/dist/tools/patch-args-normalizer.js +0 -472
- package/dist/utils/toon.d.ts +0 -4
- package/dist/utils/toon.js +0 -75
- /package/dist/{conversion/compat/actions/apply-patch-format-fixer.d.ts → servertool/handlers/apply-patch-guard.d.ts} +0 -0
- /package/dist/{conversion/shared/responses-types.js → servertool/handlers/exec-command-guard.d.ts} +0 -0
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
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
|
+
};
|
|
101
|
+
};
|
|
102
|
+
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { JsonObject } from '../../conversion/hub/types/json.js';
|
|
2
|
+
type Options = {
|
|
3
|
+
maxNonSystemMessages: number;
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* Trim OpenAI-style messages for internal servertool followups.
|
|
7
|
+
*
|
|
8
|
+
* Goal: avoid sending a massive chat history on auto-followup requests (e.g. stop_message_flow),
|
|
9
|
+
* which can push Gemini/Antigravity into empty/malformed responses under long-context loads.
|
|
10
|
+
*
|
|
11
|
+
* Strategy:
|
|
12
|
+
* - Always keep system/developer messages (regardless of position).
|
|
13
|
+
* - Keep only the tail of non-system messages (maxNonSystemMessages), preserving original order.
|
|
14
|
+
*/
|
|
15
|
+
export declare function trimOpenAiMessagesForFollowup(rawMessages: unknown, options: Options): JsonObject[];
|
|
16
|
+
export {};
|