@sema-agent/core 5.18.0 → 5.19.0
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/CHANGELOG.md +97 -0
- package/dist/core/checkpoint-store.d.ts +3 -2
- package/dist/core/fs-write-gate-policy.js +1 -1
- package/dist/core/hooks.d.ts +3 -1
- package/dist/core/hooks.js +36 -0
- package/dist/core/permission-rule-consent.d.ts +8 -1
- package/dist/core/permission-rule-consent.js +21 -10
- package/dist/core/runner/active-skill-scope.js +1 -1
- package/dist/core/runner/prepare-task.d.ts +4 -0
- package/dist/core/runner/prepare-task.js +171 -121
- package/dist/core/runner/runtask.d.ts +3 -1
- package/dist/core/runner/runtask.js +35 -7
- package/dist/core/runner/session-rule-policy.js +1 -1
- package/dist/core/sensitive-path-policy.js +1 -1
- package/dist/core/tool-policy.d.ts +6 -0
- package/dist/core/tool-policy.js +75 -24
- package/dist/core/types.d.ts +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/orchestration/run-spec.js +1 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +5 -2
- package/dist/tools/fs/bash-readonly-classifier.js +129 -17
- package/package.json +1 -1
package/dist/core/tool-policy.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
|
-
import {
|
|
2
|
+
import { join, normalize as normalizePath, posix as posixPath, sep, win32 as winPath } from "node:path";
|
|
3
3
|
import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
|
|
4
4
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
5
5
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
6
6
|
import { parsePermissionRule } from "./permission-rules.js";
|
|
7
|
-
import { writeTargetPath } from "../tools/fs/safety.js";
|
|
7
|
+
import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
8
|
+
export const APPROVAL_SETTLED_BY_VALUES = ["human", "timeout", "aborted"];
|
|
9
|
+
export function isApprovalSettledBy(v) {
|
|
10
|
+
return typeof v === "string" && APPROVAL_SETTLED_BY_VALUES.includes(v);
|
|
11
|
+
}
|
|
8
12
|
export function decisionText(d) {
|
|
9
13
|
return d.message;
|
|
10
14
|
}
|
|
@@ -19,6 +23,7 @@ export function refuseOutOfContractDecision(d) {
|
|
|
19
23
|
return d;
|
|
20
24
|
return { action: "deny", message: RETIRED_TEXT_FIELD_DENY_MESSAGE, decisionReason: "rule" };
|
|
21
25
|
}
|
|
26
|
+
const DEADLINE_ELAPSED = Symbol("approval.deadline_elapsed");
|
|
22
27
|
function withTimeout(p, ms, onTimeout) {
|
|
23
28
|
if (ms === undefined) {
|
|
24
29
|
return p;
|
|
@@ -51,6 +56,18 @@ export function createAllowDenyPolicy(opts) {
|
|
|
51
56
|
return undefined;
|
|
52
57
|
const kept = [];
|
|
53
58
|
for (const entry of entries) {
|
|
59
|
+
if (entry.startsWith("mcp__")) {
|
|
60
|
+
const segments = entry.slice("mcp__".length).split("__");
|
|
61
|
+
if (segments.some((seg) => seg.length === 0)) {
|
|
62
|
+
invalid.push({
|
|
63
|
+
entry,
|
|
64
|
+
list,
|
|
65
|
+
message: `"${entry}" is a malformed MCP tool name (empty segment) — it can never match any mounted tool. ` +
|
|
66
|
+
`Use \`mcp__<server>\` for every tool of a server, or \`mcp__<server>__<tool>\` for one tool.`,
|
|
67
|
+
});
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
54
71
|
if (parsePermissionRule(entry).ruleContent === undefined) {
|
|
55
72
|
kept.push(entry);
|
|
56
73
|
continue;
|
|
@@ -118,25 +135,37 @@ export function createApprovalPolicy(opts) {
|
|
|
118
135
|
}
|
|
119
136
|
if (need.has(toolName)) {
|
|
120
137
|
if (signal?.aborted) {
|
|
121
|
-
return { action: "deny", message: `approval aborted for "${req.toolName}" (task ended)
|
|
138
|
+
return { action: "deny", message: `approval aborted for "${req.toolName}" (task ended)`, settledBy: "aborted" };
|
|
122
139
|
}
|
|
123
140
|
let ok;
|
|
124
141
|
try {
|
|
125
|
-
ok = await withTimeout(Promise.resolve(opts.approve(req, signal)), opts.approvalTimeoutMs, () =>
|
|
142
|
+
ok = await withTimeout(Promise.resolve(opts.approve(req, signal)), opts.approvalTimeoutMs, () => DEADLINE_ELAPSED);
|
|
126
143
|
}
|
|
127
144
|
catch (err) {
|
|
128
145
|
return {
|
|
129
146
|
action: "deny",
|
|
130
147
|
message: `approval errored for "${req.toolName}": ${describeThrown(err)}`,
|
|
148
|
+
settledBy: "aborted",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
if (ok === DEADLINE_ELAPSED) {
|
|
152
|
+
return {
|
|
153
|
+
action: "deny",
|
|
154
|
+
message: `no one answered the approval request for "${req.toolName}" — the approval window elapsed ` +
|
|
155
|
+
`(${opts.approvalTimeoutMs}ms) with no answer; denied fail-closed`,
|
|
156
|
+
settledBy: "timeout",
|
|
131
157
|
};
|
|
132
158
|
}
|
|
159
|
+
if (signal?.aborted) {
|
|
160
|
+
return { action: "deny", message: `approval aborted for "${req.toolName}" (task ended)`, settledBy: "aborted" };
|
|
161
|
+
}
|
|
133
162
|
const okRaw = ok;
|
|
134
163
|
if (okRaw === true)
|
|
135
|
-
return
|
|
164
|
+
return { action: "allow", settledBy: "human" };
|
|
136
165
|
if (okRaw !== false) {
|
|
137
|
-
return { action: "deny", message: `approval callback for "${req.toolName}" returned an out-of-contract value (${typeof ok}) — denied fail-closed (this policy's \`approve\` returns a boolean: return true or false)
|
|
166
|
+
return { action: "deny", message: `approval callback for "${req.toolName}" returned an out-of-contract value (${typeof ok}) — denied fail-closed (this policy's \`approve\` returns a boolean: return true or false)`, settledBy: "aborted" };
|
|
138
167
|
}
|
|
139
|
-
return { action: "deny", message: `approval denied for "${req.toolName}"
|
|
168
|
+
return { action: "deny", message: `approval denied for "${req.toolName}"`, settledBy: "human" };
|
|
140
169
|
}
|
|
141
170
|
if (opts.denyByDefault && !auto.has(toolName)) {
|
|
142
171
|
return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
|
|
@@ -153,6 +182,7 @@ export function combinePolicies(...policies) {
|
|
|
153
182
|
let asked;
|
|
154
183
|
let current = req;
|
|
155
184
|
let rewrite;
|
|
185
|
+
let settled;
|
|
156
186
|
for (const p of policies) {
|
|
157
187
|
const d = refuseOutOfContractDecision(await p.check(current, signal));
|
|
158
188
|
if (d.action === "deny") {
|
|
@@ -162,6 +192,11 @@ export function combinePolicies(...policies) {
|
|
|
162
192
|
current = { ...current, args: d.updatedInput };
|
|
163
193
|
rewrite = d;
|
|
164
194
|
}
|
|
195
|
+
if (d.action === "allow") {
|
|
196
|
+
const supplied = d.settledBy;
|
|
197
|
+
if (supplied !== undefined)
|
|
198
|
+
settled = supplied;
|
|
199
|
+
}
|
|
165
200
|
if (d.action === "ask" && (asked === undefined || (d.requiresRealApproval === true && asked.requiresRealApproval !== true))) {
|
|
166
201
|
asked = d;
|
|
167
202
|
}
|
|
@@ -170,7 +205,8 @@ export function combinePolicies(...policies) {
|
|
|
170
205
|
const merged = rewrite?.updatedInput;
|
|
171
206
|
return merged !== undefined ? { ...asked, updatedInput: merged } : asked;
|
|
172
207
|
}
|
|
173
|
-
|
|
208
|
+
const allowed = rewrite ?? ALLOW;
|
|
209
|
+
return settled !== undefined ? { ...allowed, settledBy: settled } : allowed;
|
|
174
210
|
},
|
|
175
211
|
};
|
|
176
212
|
}
|
|
@@ -490,13 +526,16 @@ export function createTranscriptIntegrityPolicy(opts) {
|
|
|
490
526
|
const readAllow = new Set(opts?.readAllow ?? BASH_READONLY_DEFAULT_ALLOW);
|
|
491
527
|
const shellTools = canonicalToolNameSet(opts?.tools);
|
|
492
528
|
const dirSegs = dirs.map((d) => pathSegments(d).map(foldPathCase));
|
|
493
|
-
const inProtectedDir = (p) => {
|
|
494
|
-
const
|
|
529
|
+
const inProtectedDir = (p, liveCwd) => {
|
|
530
|
+
const rawRelative = !isAbsolutePathForm(p);
|
|
531
|
+
const joinInFamily = (base, rel) => isWinFormPath(base) ? winPath.join(base, rel) : posixPath.join(base, rel);
|
|
532
|
+
const abs = liveCwd !== undefined && rawRelative ? lexicalPath(joinInFamily(liveCwd, p), home) : lexicalPath(p, home);
|
|
495
533
|
const segs = pathSegments(abs).map(foldPathCase);
|
|
496
|
-
if (
|
|
497
|
-
return
|
|
498
|
-
|
|
499
|
-
|
|
534
|
+
if (isAbsolutePathForm(abs) && dirSegs.some((d) => d.length <= segs.length && d.every((s, i) => s === segs[i])))
|
|
535
|
+
return true;
|
|
536
|
+
if (!rawRelative)
|
|
537
|
+
return false;
|
|
538
|
+
return dirSegs.some((d) => relativeContinuesDirTail(pathSegments(lexicalPath(p, home)).map(foldPathCase), d));
|
|
500
539
|
};
|
|
501
540
|
const referencesProtectedDir = (cmd) => {
|
|
502
541
|
const hay = foldPathCase(cmd);
|
|
@@ -535,18 +574,21 @@ export function createTranscriptIntegrityPolicy(opts) {
|
|
|
535
574
|
if (typeof command !== "string")
|
|
536
575
|
return ALLOW;
|
|
537
576
|
const cmd = command.normalize("NFC");
|
|
538
|
-
|
|
577
|
+
const cwdInsideProtected = req.cwd !== undefined && inProtectedDir(req.cwd);
|
|
578
|
+
if (!referencesProtectedDir(cmd) && !cwdInsideProtected)
|
|
539
579
|
return ALLOW;
|
|
540
580
|
const parsed = parseLeadingCommandName(command);
|
|
541
581
|
if ("name" in parsed && readAllow.has(parsed.name))
|
|
542
582
|
return ALLOW;
|
|
543
583
|
return askReason("name" in parsed
|
|
544
|
-
? `\`${parsed.name}\` is not a read-only command`
|
|
545
|
-
:
|
|
584
|
+
? `\`${parsed.name}\` is not a read-only command${cwdInsideProtected ? " and the shell's working directory is inside the transcript directory" : ""}`
|
|
585
|
+
: cwdInsideProtected
|
|
586
|
+
? "the shell's working directory is inside the transcript directory and this is not a single read-only command (fail-closed)"
|
|
587
|
+
: "the command references the transcript directory and is not a single read-only command (fail-closed)");
|
|
546
588
|
}
|
|
547
589
|
if (toolName === "Write" || toolName === "Edit" || toolName === "NotebookEdit") {
|
|
548
590
|
const p = writeTargetPath(toolName, req.args);
|
|
549
|
-
if (typeof p === "string" && inProtectedDir(p)) {
|
|
591
|
+
if (typeof p === "string" && inProtectedDir(p, req.cwd)) {
|
|
550
592
|
return askReason(`"${p}" is inside the session-transcript directory`);
|
|
551
593
|
}
|
|
552
594
|
}
|
|
@@ -643,7 +685,7 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
643
685
|
};
|
|
644
686
|
}
|
|
645
687
|
if (signal?.aborted) {
|
|
646
|
-
return { action: "deny", message: `approval aborted for "${req.toolName}" (task ended)`, decisionReason: "mode" };
|
|
688
|
+
return { action: "deny", message: `approval aborted for "${req.toolName}" (task ended)`, decisionReason: "mode", settledBy: "aborted" };
|
|
647
689
|
}
|
|
648
690
|
const presented = tryCloneArgs(req.args);
|
|
649
691
|
if (!presented.ok) {
|
|
@@ -651,6 +693,7 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
651
693
|
action: "deny",
|
|
652
694
|
message: `approval for "${req.toolName}" could not present the args safely (unclonable value: ${presented.reason}) — denied fail-closed`,
|
|
653
695
|
decisionReason: "mode",
|
|
696
|
+
settledBy: "aborted",
|
|
654
697
|
};
|
|
655
698
|
}
|
|
656
699
|
let ok;
|
|
@@ -661,6 +704,7 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
661
704
|
action: "deny",
|
|
662
705
|
message: `approval for "${req.toolName}" could not present the args safely (unclonable value: ${approverView.reason}) — denied fail-closed`,
|
|
663
706
|
decisionReason: "mode",
|
|
707
|
+
settledBy: "aborted",
|
|
664
708
|
};
|
|
665
709
|
}
|
|
666
710
|
ok = await onAsk({ ...req, boundInputHash: boundInputHashOf(presented.value), args: approverView.value }, signal);
|
|
@@ -670,42 +714,49 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
670
714
|
action: "deny",
|
|
671
715
|
message: `approval errored for "${req.toolName}": ${describeThrown(err)}`,
|
|
672
716
|
decisionReason: "mode",
|
|
717
|
+
settledBy: "aborted",
|
|
673
718
|
};
|
|
674
719
|
}
|
|
720
|
+
if (signal?.aborted) {
|
|
721
|
+
return { action: "deny", message: `approval aborted for "${req.toolName}" (task ended)`, decisionReason: "mode", settledBy: "aborted" };
|
|
722
|
+
}
|
|
675
723
|
if (ok === "unavailable") {
|
|
676
724
|
return {
|
|
677
725
|
action: "deny",
|
|
678
726
|
message: `approval required for "${req.toolName}" but the approver reported unavailable (no reachable ` +
|
|
679
727
|
`operator for this ask) and no durable approval gate is armed — denied fail-closed: ${req.message}`,
|
|
680
728
|
decisionReason: "mode",
|
|
729
|
+
settledBy: "aborted",
|
|
681
730
|
approverUnavailable: true,
|
|
682
731
|
};
|
|
683
732
|
}
|
|
684
733
|
if (typeof ok === "object" && ok !== null) {
|
|
685
734
|
if (ok.allow !== true) {
|
|
686
|
-
return { action: "deny", message: `approval denied for "${req.toolName}": ${req.message}`, decisionReason: "mode" };
|
|
735
|
+
return { action: "deny", message: `approval denied for "${req.toolName}": ${req.message}`, decisionReason: "mode", settledBy: "human" };
|
|
687
736
|
}
|
|
688
737
|
if (ok.updatedInput === undefined)
|
|
689
|
-
return { action: "allow", decisionReason: "mode", presentedInput: presented.value };
|
|
738
|
+
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settledBy: "human" };
|
|
690
739
|
const edit = tryCloneArgs(ok.updatedInput);
|
|
691
740
|
if (!edit.ok) {
|
|
692
741
|
return {
|
|
693
742
|
action: "deny",
|
|
694
743
|
message: `the approved edit for "${req.toolName}" is not safely clonable (${edit.reason}) — denied fail-closed`,
|
|
695
744
|
decisionReason: "mode",
|
|
745
|
+
settledBy: "aborted",
|
|
696
746
|
};
|
|
697
747
|
}
|
|
698
|
-
return { action: "allow", updatedInput: edit.value, decisionReason: "mode" };
|
|
748
|
+
return { action: "allow", updatedInput: edit.value, decisionReason: "mode", settledBy: "human" };
|
|
699
749
|
}
|
|
700
750
|
const okRaw = ok;
|
|
701
751
|
if (okRaw === true)
|
|
702
|
-
return { action: "allow", decisionReason: "mode", presentedInput: presented.value };
|
|
752
|
+
return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settledBy: "human" };
|
|
703
753
|
if (okRaw !== false) {
|
|
704
754
|
return {
|
|
705
755
|
action: "deny",
|
|
706
756
|
message: `the approver for "${req.toolName}" returned an out-of-contract value (${typeof ok}) — denied fail-closed (return true, false, "unavailable", or the {allow} object)`,
|
|
707
757
|
decisionReason: "mode",
|
|
758
|
+
settledBy: "aborted",
|
|
708
759
|
};
|
|
709
760
|
}
|
|
710
|
-
return { action: "deny", message: `approval denied for "${req.toolName}": ${req.message}`, decisionReason: "mode" };
|
|
761
|
+
return { action: "deny", message: `approval denied for "${req.toolName}": ${req.message}`, decisionReason: "mode", settledBy: "human" };
|
|
711
762
|
}
|
package/dist/core/types.d.ts
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -122,14 +122,14 @@ export type { SchedulerCapability, SchedulerErrorCode, ScheduledIntent, Schedule
|
|
|
122
122
|
export { createSchedulerTools, type SchedulerToolContext, SCHEDULE_WAKEUP_TOOL_NAME, AUTONOMOUS_LOOP_SENTINEL, AUTONOMOUS_LOOP_DYNAMIC_SENTINEL, } from "./tools/scheduler-tools.js";
|
|
123
123
|
export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT, type AutonomousLoopPromptOptions, } from "./tools/loop-tick.js";
|
|
124
124
|
export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
|
|
125
|
-
export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, } from "./core/tool-policy.js";
|
|
125
|
+
export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, type ToolPolicyNameSets, type NamedToolPolicy, type ToolPolicy, type ToolCallRequest, type PermissionResult, type DecisionReason, type ApprovalSettledBy, APPROVAL_SETTLED_BY_VALUES, isApprovalSettledBy, type OnAsk, type AskOutcome, type ResolvedAsk, type AskRequest, type AskDelegationProvenance, } from "./core/tool-policy.js";
|
|
126
126
|
export { parseAutoModeResponse, createAutoModeDecider, type AutoModeVerdict, type AutoModeDecider, type AutoModeDeciderOptions, type AutoModeClassifyFn, type AutoModeClassifyInput, } from "./core/auto-mode.js";
|
|
127
127
|
export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, type AutoModeRules, type BuildAutoModePromptOptions, type AutoModeWindowOptions, } from "./core/auto-mode-prompt.js";
|
|
128
128
|
export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
|
|
129
129
|
export { createPermissionRulePolicy, validatePermissionRules, parsePermissionRule, wildcardMatch, type PermissionRule, type ParsedPermissionRule, type PermissionRuleIssue, type PermissionRuleCaps, type PermissionRulePolicyOptions, } from "./core/permission-rules.js";
|
|
130
130
|
export { parseAllowRuleText, formatAllowRuleText, ruleAdmitsCommand, findAdmittingRule, suggestRulesForCommand, scopeCoversCwd, pathWithinRoot, isRuleLive, BARE_INTERPRETER_NAMES, MAX_RULE_TEXT_CHARS, type PersistedAllowRule, type RuleTombstone, type RuleScope, type RuleDot, type RuleAdd, type RuleAddOrigin, type RuleSuggestion, type RuleReject, type RuleRejectCode, type ParsedAllowRule, type PersistedRuleTool, type PersistedRuleMatch, } from "./core/permission-rule-model.js";
|
|
131
131
|
export { removePersistedRule, applyTombstones, sameScope, InMemoryPermissionRuleStore, EMPTY_RULE_STORE, type PermissionRuleStore, type PermissionRuleStoreProvider, type StoredAllowRules, type RemoveResult, type PutResult, } from "./core/permission-rule-store.js";
|
|
132
|
-
export { prepareCardApproval, confirmRuleApproval, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, type RuleTicket, type RuleCandidate, type RuleApprovalKind, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleConsentDeps, type RedeemResult, type CcImportLayer, type ImportedSettingsLayer, type ImportPreview, type ImportResult, } from "./core/permission-rule-consent.js";
|
|
132
|
+
export { prepareCardApproval, confirmRuleApproval, type ConfirmResult, type ConfirmRefusalReason, redeemRuleTicket, redeemRuleBatch, prepareCcImport, prepareStarterBatch, mintRuleTicket, STARTER_RULES, InMemoryRuleApprovalRecordStore, type RuleTicket, type RuleCandidate, type RuleApprovalKind, type RuleApprovalRecord, type RuleApprovalRecordStore, type RuleConsentDeps, type RedeemResult, type CcImportLayer, type ImportedSettingsLayer, type ImportPreview, type ImportResult, } from "./core/permission-rule-consent.js";
|
|
133
133
|
export { FilePermissionRuleStoreProvider } from "./stores/file/permission-rule-store.js";
|
|
134
134
|
export { formatHookFeedback, runToolGate, createHookEnvCapabilities, type Hooks, type HookToolContext, type HookEnvCapabilities, type HookToolOutput, type PreToolUseResult, type PostToolUseResult, type UserPromptSubmitResult, type HookToolFailure, type PostToolUseFailureResult, type PostToolBatchCall, type PostToolBatchResult, type PreCompactContext, type PreCompactResult, type PostCompactContext, type StopFailureContext, type PermissionDeniedPayload, type PermissionDeniedSource, } from "./core/hooks.js";
|
|
135
135
|
export { InMemoryMemoryStore, composeMemoryBlock, composeLayeredMemoryBlock, normalizeMemorySpec, type NormalizedMemorySpec, type MemorySpecInput, supportsConsolidation, supportsPeriodicConsolidation, firstSentence, expandLexicalTerms, lexicalSearchMatch, type Embedder, classifyPromotable, enforcePromotableWriteGate, guardedMemoryStore, MemoryGateError, detectSecret, enforceSecretWriteGate, enforceStructuredNoteSecretGate, type UtilityGate, type MemoryStore, type MemoryVectorMode, type ScoredMemory, type MemoryNoteHeader, type MemoryNoteRecord, type MemoryNoteType, type StructuredNoteInput, } from "./core/memory.js";
|
package/dist/index.js
CHANGED
|
@@ -107,7 +107,7 @@ export { hasScheduler, isValidCronExpr, SchedulerError } from "./core/scheduler.
|
|
|
107
107
|
export { createSchedulerTools, SCHEDULE_WAKEUP_TOOL_NAME, AUTONOMOUS_LOOP_SENTINEL, AUTONOMOUS_LOOP_DYNAMIC_SENTINEL, } from "./tools/scheduler-tools.js";
|
|
108
108
|
export { resolveAutonomousLoopPrompt, AUTONOMOUS_LOOP_PREAMBLE, AUTONOMOUS_LOOP_PREAMBLE_PERSISTENT, } from "./tools/loop-tick.js";
|
|
109
109
|
export { tightenTaskSpec, TaskSpecTightenError } from "./core/tighten-task-spec.js";
|
|
110
|
-
export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, } from "./core/tool-policy.js";
|
|
110
|
+
export { createAllowDenyPolicy, createApprovalPolicy, COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, findUnverifiableRecursiveDelete, combinePolicies, decisionText, resolveAsk, toolPolicyNameSets, APPROVAL_SETTLED_BY_VALUES, isApprovalSettledBy, } from "./core/tool-policy.js";
|
|
111
111
|
export { parseAutoModeResponse, createAutoModeDecider, } from "./core/auto-mode.js";
|
|
112
112
|
export { buildAutoModePrompt, renderAutoModeWindow, renderAutoModeAction, AUTO_MODE_DEFAULTS_SENTINEL, } from "./core/auto-mode-prompt.js";
|
|
113
113
|
export { AUTO_MODE_BASE_PROMPT, AUTO_MODE_PERMISSIONS_EXTERNAL } from "./core/auto-mode-prompt-assets.js";
|
|
@@ -26,7 +26,7 @@ function frozenDenyPolicy(rootDir, frozenResolved) {
|
|
|
26
26
|
if (typeof raw !== "string" || raw.length === 0)
|
|
27
27
|
return { action: "allow" };
|
|
28
28
|
const absForm = isAbsolutePathForm(raw);
|
|
29
|
-
const abs = pathKey(absForm ? raw : resolve(rootDir, raw));
|
|
29
|
+
const abs = pathKey(absForm ? raw : resolve(req.cwd || rootDir, raw));
|
|
30
30
|
if (frozen.has(abs)) {
|
|
31
31
|
return { action: "deny", message: `${raw} is part of the frozen specification surface (read-only).` };
|
|
32
32
|
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
export declare const NOT_AUTO_ALLOWED = "\u2014 not auto-allowed";
|
|
2
2
|
export declare const BASH_READONLY_DEFAULT_ALLOW: readonly string[];
|
|
3
|
-
export
|
|
3
|
+
export interface LeadingCommandNameOptions {
|
|
4
|
+
quotedOperatorsAreText?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export declare function parseLeadingCommandName(command: string, options?: LeadingCommandNameOptions): {
|
|
4
7
|
name: string;
|
|
5
8
|
} | {
|
|
6
9
|
reject: string;
|
|
7
10
|
};
|
|
8
|
-
export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string
|
|
11
|
+
export declare function coarseReadonlyCheck(command: string, allow: ReadonlySet<string>, options?: LeadingCommandNameOptions): string | undefined;
|
|
9
12
|
export interface BashReadonlyRootBoundary {
|
|
10
13
|
roots: readonly string[];
|
|
11
14
|
cwd?: string;
|
|
@@ -5,11 +5,41 @@ export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
|
5
5
|
"grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
|
|
6
6
|
];
|
|
7
7
|
const SHELL_OPERATORS = /[;&|<>$()`\n\r\\]/;
|
|
8
|
-
|
|
8
|
+
function quoteMask(s) {
|
|
9
|
+
const quoted = new Array(s.length).fill(false);
|
|
10
|
+
let open;
|
|
11
|
+
for (let i = 0; i < s.length; i++) {
|
|
12
|
+
const ch = s[i];
|
|
13
|
+
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
14
|
+
open = ch;
|
|
15
|
+
quoted[i] = true;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
if (open !== undefined) {
|
|
19
|
+
quoted[i] = true;
|
|
20
|
+
if (open === ch)
|
|
21
|
+
open = undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return { quoted, balanced: open === undefined };
|
|
25
|
+
}
|
|
26
|
+
function hasBareShellOperator(s, quotedOperatorsAreText) {
|
|
27
|
+
if (!quotedOperatorsAreText)
|
|
28
|
+
return SHELL_OPERATORS.test(s);
|
|
29
|
+
const mask = quoteMask(s);
|
|
30
|
+
if (!mask.balanced)
|
|
31
|
+
return SHELL_OPERATORS.test(s);
|
|
32
|
+
for (let i = 0; i < s.length; i++) {
|
|
33
|
+
if (!mask.quoted[i] && SHELL_OPERATORS.test(s[i]))
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
export function parseLeadingCommandName(command, options) {
|
|
9
39
|
const trimmed = command.trim();
|
|
10
40
|
if (!trimmed)
|
|
11
41
|
return { reject: "empty command" };
|
|
12
|
-
if (
|
|
42
|
+
if (hasBareShellOperator(trimmed, options?.quotedOperatorsAreText === true)) {
|
|
13
43
|
return {
|
|
14
44
|
reject: "shell operators (pipes, redirects, ;, &&, command substitution, subshells, variable expansion) are not allowed — run a single simple command",
|
|
15
45
|
};
|
|
@@ -23,8 +53,8 @@ export function parseLeadingCommandName(command) {
|
|
|
23
53
|
return { reject: "the command name must be a bare token (no quotes, braces, globs, or ~)" };
|
|
24
54
|
return { name: first };
|
|
25
55
|
}
|
|
26
|
-
export function coarseReadonlyCheck(command, allow) {
|
|
27
|
-
const parsed = parseLeadingCommandName(command);
|
|
56
|
+
export function coarseReadonlyCheck(command, allow, options) {
|
|
57
|
+
const parsed = parseLeadingCommandName(command, options);
|
|
28
58
|
if ("reject" in parsed)
|
|
29
59
|
return parsed.reject;
|
|
30
60
|
if (!allow.has(parsed.name))
|
|
@@ -32,6 +62,35 @@ export function coarseReadonlyCheck(command, allow) {
|
|
|
32
62
|
return undefined;
|
|
33
63
|
}
|
|
34
64
|
const SHELL_SEGMENT_HARD_REJECT = /[<>$()`\n\r\\]/;
|
|
65
|
+
const EXEMPT_REDIRECTION = /^(?:2>\/dev\/null|[12]?>&[12])$/;
|
|
66
|
+
function stripExemptRedirections(command) {
|
|
67
|
+
if (!command.includes(">"))
|
|
68
|
+
return command;
|
|
69
|
+
const mask = quoteMask(command);
|
|
70
|
+
if (!mask.balanced)
|
|
71
|
+
return command;
|
|
72
|
+
let out = "";
|
|
73
|
+
let word = "";
|
|
74
|
+
let wordQuoted = false;
|
|
75
|
+
const flush = () => {
|
|
76
|
+
out += wordQuoted || !EXEMPT_REDIRECTION.test(word) ? word : "";
|
|
77
|
+
word = "";
|
|
78
|
+
wordQuoted = false;
|
|
79
|
+
};
|
|
80
|
+
for (let i = 0; i < command.length; i++) {
|
|
81
|
+
const ch = command[i];
|
|
82
|
+
if (!mask.quoted[i] && (ch === " " || ch === "\t")) {
|
|
83
|
+
flush();
|
|
84
|
+
out += ch;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (mask.quoted[i])
|
|
88
|
+
wordQuoted = true;
|
|
89
|
+
word += ch;
|
|
90
|
+
}
|
|
91
|
+
flush();
|
|
92
|
+
return out;
|
|
93
|
+
}
|
|
35
94
|
function foldQuoteRemovalToken(tok) {
|
|
36
95
|
if (!tok.includes('"') && !tok.includes("'"))
|
|
37
96
|
return tok;
|
|
@@ -77,12 +136,30 @@ function resolveOperandLexically(base, operand, homeDir) {
|
|
|
77
136
|
return normalizeAbsPathLexically(`${base.replace(/[/\\]+$/, "")}/${raw}`);
|
|
78
137
|
}
|
|
79
138
|
function tokenizeSegment(segment) {
|
|
80
|
-
const raw = segment
|
|
81
|
-
.trim()
|
|
82
|
-
.split(/\s+/)
|
|
83
|
-
.filter((t) => t.length > 0);
|
|
139
|
+
const raw = splitWordsQuoteAware(segment);
|
|
84
140
|
return { folded: raw.map(foldQuoteRemovalToken), raw };
|
|
85
141
|
}
|
|
142
|
+
function splitWordsQuoteAware(segment) {
|
|
143
|
+
const s = segment.trim();
|
|
144
|
+
const mask = quoteMask(s);
|
|
145
|
+
if (!mask.balanced)
|
|
146
|
+
return s.split(/\s+/).filter((t) => t.length > 0);
|
|
147
|
+
const out = [];
|
|
148
|
+
let cur = "";
|
|
149
|
+
for (let i = 0; i < s.length; i++) {
|
|
150
|
+
const ch = s[i];
|
|
151
|
+
if (!mask.quoted[i] && /\s/.test(ch)) {
|
|
152
|
+
if (cur.length > 0)
|
|
153
|
+
out.push(cur);
|
|
154
|
+
cur = "";
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
cur += ch;
|
|
158
|
+
}
|
|
159
|
+
if (cur.length > 0)
|
|
160
|
+
out.push(cur);
|
|
161
|
+
return out;
|
|
162
|
+
}
|
|
86
163
|
function hasUnquotedExpansionMetachar(rawToken) {
|
|
87
164
|
let open;
|
|
88
165
|
for (const ch of rawToken) {
|
|
@@ -147,6 +224,26 @@ function isCutDelimiterPayloadLongOption(tok) {
|
|
|
147
224
|
const name = longOptionNameOf(tok);
|
|
148
225
|
return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
|
|
149
226
|
}
|
|
227
|
+
function isGrepClusterFileStdin(tok) {
|
|
228
|
+
if (tok.startsWith("--") || !/^-[A-Za-z0-9]/.test(tok))
|
|
229
|
+
return false;
|
|
230
|
+
for (let i = 1; i < tok.length; i++) {
|
|
231
|
+
const ch = tok[i];
|
|
232
|
+
if (ch === "e")
|
|
233
|
+
return false;
|
|
234
|
+
if (ch === "f")
|
|
235
|
+
return tok.slice(i + 1) === "-";
|
|
236
|
+
if (!/[A-Za-z0-9]/.test(ch))
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function isCutSeparatedDelimiterOption(tok) {
|
|
242
|
+
if (tok.includes("="))
|
|
243
|
+
return false;
|
|
244
|
+
const name = longOptionNameOf(tok);
|
|
245
|
+
return name !== undefined && (isLongOptionAbbrevOf(name, "delimiter") || isLongOptionAbbrevOf(name, "output-delimiter"));
|
|
246
|
+
}
|
|
150
247
|
function isGrepFileStdinLongOption(tok) {
|
|
151
248
|
const eq = tok.indexOf("=");
|
|
152
249
|
if (eq < 0 || tok.slice(eq + 1) !== "-")
|
|
@@ -285,16 +382,22 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
285
382
|
const trimmed = command.trim();
|
|
286
383
|
if (!trimmed)
|
|
287
384
|
return { reason: "empty command" };
|
|
288
|
-
|
|
385
|
+
const redirectionStripped = stripExemptRedirections(trimmed);
|
|
386
|
+
if (SHELL_SEGMENT_HARD_REJECT.test(redirectionStripped)) {
|
|
289
387
|
return { reason: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
|
|
290
388
|
}
|
|
291
|
-
const source =
|
|
389
|
+
const source = redirectionStripped.endsWith(";") ? redirectionStripped.slice(0, -1) : redirectionStripped;
|
|
292
390
|
const segments = [];
|
|
293
391
|
const pipeFed = [false];
|
|
392
|
+
const mask = quoteMask(source);
|
|
393
|
+
const quoted = (i) => mask.balanced && mask.quoted[i] === true;
|
|
294
394
|
let cur = "";
|
|
295
395
|
for (let i = 0; i < source.length; i++) {
|
|
296
396
|
const c = source[i];
|
|
297
|
-
if (
|
|
397
|
+
if (quoted(i)) {
|
|
398
|
+
cur += c;
|
|
399
|
+
}
|
|
400
|
+
else if (c === "&") {
|
|
298
401
|
if (source[i + 1] === "&") {
|
|
299
402
|
segments.push(cur);
|
|
300
403
|
cur = "";
|
|
@@ -327,7 +430,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
327
430
|
}
|
|
328
431
|
segments.push(cur);
|
|
329
432
|
for (const segment of segments) {
|
|
330
|
-
const reason = coarseReadonlyCheck(segment, allow);
|
|
433
|
+
const reason = coarseReadonlyCheck(segment, allow, { quotedOperatorsAreText: true });
|
|
331
434
|
if (reason !== undefined)
|
|
332
435
|
return { reason };
|
|
333
436
|
}
|
|
@@ -368,21 +471,21 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
368
471
|
continue;
|
|
369
472
|
foldedSegments.push(segmentTokens);
|
|
370
473
|
const name = toks[0];
|
|
371
|
-
const nonOption = toks.slice(1).filter((t) => !t.startsWith("-"));
|
|
372
474
|
if (!(pipeFed[si] ?? false)) {
|
|
373
475
|
const floor = STDIN_FILE_FLOOR[name];
|
|
374
476
|
const restArgs = toks.slice(1);
|
|
375
477
|
const grepPatternSuppliedByFlag = name === "grep" && restArgs.some(isGrepPatternFlagToken);
|
|
376
478
|
let sawNonFlagOperand = false;
|
|
377
479
|
let hasStdinDash = false;
|
|
480
|
+
let operandCount = 0;
|
|
378
481
|
if (name !== "tr") {
|
|
379
482
|
for (let k = 0; k < restArgs.length; k++) {
|
|
380
483
|
const t = restArgs[k];
|
|
381
|
-
if (name === "cut" && (t === "-d" || t
|
|
484
|
+
if (name === "cut" && (t === "-d" || isCutSeparatedDelimiterOption(t))) {
|
|
382
485
|
k++;
|
|
383
486
|
continue;
|
|
384
487
|
}
|
|
385
|
-
if (name === "grep" && (t
|
|
488
|
+
if (name === "grep" && (isGrepClusterFileStdin(t) || isGrepFileStdinLongOption(t))) {
|
|
386
489
|
hasStdinDash = true;
|
|
387
490
|
break;
|
|
388
491
|
}
|
|
@@ -390,6 +493,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
390
493
|
continue;
|
|
391
494
|
const isGrepPatternPosition = name === "grep" && !grepPatternSuppliedByFlag && !sawNonFlagOperand;
|
|
392
495
|
sawNonFlagOperand = true;
|
|
496
|
+
operandCount++;
|
|
393
497
|
if (isGrepPatternPosition)
|
|
394
498
|
continue;
|
|
395
499
|
if (t === "-") {
|
|
@@ -401,7 +505,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
401
505
|
if (floor !== undefined && hasStdinDash) {
|
|
402
506
|
return { reason: `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout ${NOT_AUTO_ALLOWED}` };
|
|
403
507
|
}
|
|
404
|
-
if (floor !== undefined &&
|
|
508
|
+
if (floor !== undefined && operandCount < floor) {
|
|
405
509
|
return { reason: `"${name}" with no file argument reads stdin and would block until the tool timeout ${NOT_AUTO_ALLOWED}` };
|
|
406
510
|
}
|
|
407
511
|
}
|
|
@@ -409,7 +513,15 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary) {
|
|
|
409
513
|
return { reason: "`tail` in follow mode never terminates " + NOT_AUTO_ALLOWED };
|
|
410
514
|
}
|
|
411
515
|
const GENERATOR_DEVICES = new Set(["/dev/zero", "/dev/random", "/dev/urandom", "/dev/full"]);
|
|
412
|
-
const
|
|
516
|
+
const deviceCandidates = [];
|
|
517
|
+
for (const t of toks.slice(1)) {
|
|
518
|
+
if (t.startsWith("-") && t !== "-") {
|
|
519
|
+
deviceCandidates.push(...attachedOptionPayloads(t));
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
deviceCandidates.push(t);
|
|
523
|
+
}
|
|
524
|
+
const deviceArgs = deviceCandidates.map(normalizeAbsPathLexically).filter(isBlockedDevicePath);
|
|
413
525
|
const rescuedByHead = headBoundIsSmall(name, toks) && deviceArgs.every((d) => GENERATOR_DEVICES.has(d));
|
|
414
526
|
if (!rescuedByHead && deviceArgs.length > 0) {
|
|
415
527
|
return { reason: "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) " + NOT_AUTO_ALLOWED };
|