@sema-agent/core 7.6.3 → 7.8.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 +70 -10
- package/dist/agents/agent-transcript-tool.js +2 -2
- package/dist/agents/list-agents-tool.js +2 -3
- package/dist/agents/observer.js +2 -2
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/agents/send-message-tool.js +2 -1
- package/dist/agents/subagent-steps.js +5 -4
- package/dist/agents/subagent.js +2 -3
- package/dist/brain/anthropic.js +8 -2
- package/dist/brain/open-responses.js +5 -3
- package/dist/brain/openai.js +31 -8
- package/dist/brain/reasoning.d.ts +32 -0
- package/dist/brain/reasoning.js +18 -0
- package/dist/core/ask-question.js +2 -2
- package/dist/core/auto-mode-defaults.d.ts +16 -0
- package/dist/core/auto-mode-defaults.js +1 -0
- package/dist/core/auto-mode.d.ts +19 -0
- package/dist/core/auto-mode.js +74 -56
- package/dist/core/checkpoint-execution-record.d.ts +110 -0
- package/dist/core/checkpoint-execution-record.js +49 -0
- package/dist/core/checkpoint-store.d.ts +114 -2
- package/dist/core/checkpoint-store.js +35 -2
- package/dist/core/context-edit.d.ts +0 -22
- package/dist/core/context-edit.js +2 -11
- package/dist/core/engine-notice.d.ts +17 -0
- package/dist/core/fs-write-gate-policy.d.ts +7 -1
- package/dist/core/fs-write-gate-policy.js +7 -7
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +4 -3
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +8 -0
- package/dist/core/hooks.d.ts +8 -2
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp.js +2 -2
- package/dist/core/mcp-server-spec.d.ts +17 -0
- package/dist/core/mcp.js +3 -1
- package/dist/core/memory-engine/tools.js +4 -12
- package/dist/core/permission-rules.d.ts +32 -2
- package/dist/core/permission-rules.js +43 -37
- package/dist/core/present-plan-tool.js +3 -4
- package/dist/core/runner/active-skill-scope.js +7 -4
- package/dist/core/runner/clock-and-limits.d.ts +117 -0
- package/dist/core/runner/clock-and-limits.js +118 -0
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/contracts.d.ts +37 -3
- package/dist/core/runner/decide-continuation.d.ts +98 -0
- package/dist/core/runner/decide-continuation.js +133 -0
- package/dist/core/runner/execution-record.d.ts +26 -0
- package/dist/core/runner/execution-record.js +19 -0
- package/dist/core/runner/git-leg-delivery.d.ts +28 -0
- package/dist/core/runner/git-leg-delivery.js +94 -0
- package/dist/core/runner/initial-run-state.d.ts +14 -0
- package/dist/core/runner/initial-run-state.js +11 -0
- package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
- package/dist/core/runner/prepare-caps-and-workflow.js +33 -11
- package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
- package/dist/core/runner/prepare-defer-classify.js +10 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
- package/dist/core/runner/prepare-delegation-surface.js +15 -17
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
- package/dist/core/runner/prepare-gate-stations.js +3 -1
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-lsp.d.ts +4 -3
- package/dist/core/runner/prepare-lsp.js +3 -2
- package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
- package/dist/core/runner/prepare-memory-engine-session.js +4 -2
- package/dist/core/runner/prepare-offload-wrappers.js +7 -5
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-project-context.d.ts +5 -23
- package/dist/core/runner/prepare-project-context.js +7 -5
- package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
- package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
- package/dist/core/runner/prepare-protocol-tools.js +38 -21
- package/dist/core/runner/prepare-question-face.d.ts +4 -3
- package/dist/core/runner/prepare-question-face.js +5 -3
- package/dist/core/runner/prepare-safety-scan.js +42 -2
- package/dist/core/runner/prepare-task.js +37 -19
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
- package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
- package/dist/core/runner/prepare-wiring-manifest.js +3 -8
- package/dist/core/runner/run-harness-handlers.d.ts +46 -0
- package/dist/core/runner/run-harness-handlers.js +345 -0
- package/dist/core/runner/run-turn-boundary.d.ts +69 -0
- package/dist/core/runner/run-turn-boundary.js +694 -0
- package/dist/core/runner/runtask.d.ts +0 -68
- package/dist/core/runner/runtask.js +66 -1486
- package/dist/core/runner/session-rule-policy.js +9 -4
- package/dist/core/runner/steer-admission.d.ts +17 -0
- package/dist/core/runner/steer-admission.js +17 -0
- package/dist/core/runner/synthetic-tools.js +5 -8
- package/dist/core/runner/tool-disclosure.d.ts +4 -0
- package/dist/core/runner/tool-disclosure.js +9 -5
- package/dist/core/runner/tool-end-body.d.ts +71 -0
- package/dist/core/runner/tool-end-body.js +75 -0
- package/dist/core/runner/tool-face-overlay.d.ts +40 -0
- package/dist/core/runner/tool-face-overlay.js +125 -0
- package/dist/core/runner/tool-output-projection.d.ts +10 -0
- package/dist/core/runner/tool-output-projection.js +33 -10
- package/dist/core/sensitive-path-policy.d.ts +3 -0
- package/dist/core/sensitive-path-policy.js +6 -5
- package/dist/core/shared-memory/tools.js +3 -6
- package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +108 -0
- package/dist/core/task-event.d.ts +10 -0
- package/dist/core/task-registry.js +4 -5
- package/dist/core/task-spec.d.ts +8 -0
- package/dist/core/tool-catalog-entries.d.ts +48 -0
- package/dist/core/tool-catalog-entries.js +174 -0
- package/dist/core/tool-conformance.d.ts +22 -0
- package/dist/core/tool-conformance.js +37 -0
- package/dist/core/tool-face.d.ts +171 -0
- package/dist/core/tool-face.js +25 -0
- package/dist/core/tool-policy.d.ts +24 -0
- package/dist/core/tool-policy.js +4 -2
- package/dist/core/tool-registry.d.ts +114 -0
- package/dist/core/tool-registry.js +114 -0
- package/dist/core/tool-result-budget.d.ts +0 -3
- package/dist/core/tool-result-budget.js +4 -3
- package/dist/core/tool-result-store.d.ts +0 -18
- package/dist/core/tool-result-store.js +2 -18
- package/dist/core/tool-roster.d.ts +450 -0
- package/dist/core/tool-roster.js +497 -0
- package/dist/core/tool-spec.d.ts +54 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/trace.d.ts +24 -0
- package/dist/core/wiring-manifest.d.ts +13 -0
- package/dist/core/wiring-manifest.js +2 -1
- package/dist/core/write-protect.d.ts +2 -1
- package/dist/core/write-protect.js +5 -4
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +19 -19
- package/dist/engine/loop/types.d.ts +6 -0
- package/dist/index.d.ts +20 -8
- package/dist/index.js +19 -7
- package/dist/orchestration/goal.js +2 -1
- package/dist/orchestration/run-spec.js +3 -5
- package/dist/orchestration/run-workflow-tool.js +2 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
- package/dist/stores/file/checkpoint-store.d.ts +7 -0
- package/dist/stores/file/checkpoint-store.js +20 -1
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +7 -11
- package/dist/tools/fs/fs-read.js +2 -2
- package/dist/tools/fs/fs-search-tools.d.ts +0 -7
- package/dist/tools/fs/fs-search-tools.js +3 -16
- package/dist/tools/fs/fs-write.js +4 -6
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +2 -1
- package/dist/tools/fs/safety.js +6 -5
- package/dist/tools/monitor.js +2 -2
- package/dist/tools/scheduler-tools.js +5 -8
- package/dist/tools/task-list.js +5 -8
- package/dist/tools/todo.js +2 -2
- package/dist/tools/web.js +3 -6
- package/dist/tools/worktree.js +3 -4
- package/package.json +4 -1
- package/test/export-surface.snapshot.json +183 -7
|
@@ -17,6 +17,12 @@ export interface EngineNotice {
|
|
|
17
17
|
* the value in force; `detail: { knob, raw, usedMs }`.
|
|
18
18
|
* - `"config.materialize_env_discarded"` — `SEMA_TOOL_MATERIALIZE_STRATEGY` held a value outside the
|
|
19
19
|
* closed set in a seat where it is not in force; `detail: { raw, specStrategy? }`.
|
|
20
|
+
* - `"config.tool_card_undeclared"` (design/388 L4) — a tool minted a `details.type` card its definition
|
|
21
|
+
* (or its caller declaration's `renderHints.resultCards`) never declared; the card was dropped from
|
|
22
|
+
* `tool_end.structured`; once per leg per type; `detail: { type, sessionId, runId }`.
|
|
23
|
+
* - `"config.tool_face_undeclared"` / `"config.tool_face_invalid"` (design/388 §2.7) — an MCP server's
|
|
24
|
+
* `toolFaces` overlay names a tool the listing does not carry / does not fit the tool's materialized
|
|
25
|
+
* schema (the tool mounts bare); once per leg per tool; `detail: { server, tool, problem?, sessionId, runId }`.
|
|
20
26
|
* - `"tool_result.offload_put_failed"` (#167) — a clear-with-offload persist's fire-and-forget put
|
|
21
27
|
* failed; THIS attempt stored nothing (the failure arm reports, it never re-inserts under the
|
|
22
28
|
* ref) — an earlier attempt of the same idempotent re-put may already have stored the row, so
|
|
@@ -211,6 +217,17 @@ export interface EngineNotice {
|
|
|
211
217
|
* (whoever answered the card is the one entitled to hear the answer ran nothing); a host may
|
|
212
218
|
* forward it on its own wire.
|
|
213
219
|
*
|
|
220
|
+
* - `"checkpoint.execution_outcome_unrecorded"` — the resumed leg's pending-call resolver disposed
|
|
221
|
+
* the decided action (executed it, delivered the refusal, or vetoed the person's yes) and filed
|
|
222
|
+
* the frame's record through `CheckpointStore.recordExecutionOutcome`, and the store answered
|
|
223
|
+
* `not_resolved` or `absent` — the row was reopened, expired or removed under the leg by another
|
|
224
|
+
* actor, so the row now reads `unknown` on its execution axis (`executionVerdict`). The
|
|
225
|
+
* execution result is NOT changed by the answer (the record is an account of what ran, never a
|
|
226
|
+
* gate on it); the notice is the loud half. Once per such resolution;
|
|
227
|
+
* `detail: { sessionId, runId, scope, checkpointId?, word }` (`word` = the store's answer; the
|
|
228
|
+
* secret token never rides a notice). A DIFFERENT record already on file is not this notice — it
|
|
229
|
+
* is the `checkpoint.execution_outcome_conflict` throw (an engine defect, propagated).
|
|
230
|
+
*
|
|
214
231
|
* - `"classifier.denial_limit"` (#548, CC 2.1.250 `FO`/"too many classifier denials in headless
|
|
215
232
|
* mode") — the auto-mode classifier's DENIAL LIMIT was reached (3 consecutive blocks, or 20 in the
|
|
216
233
|
* run; `RunnerDeps.autoMode.denialLimit`) and the fallback ask it turns into had NO approver to go
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ExecutionEnv } from "../internal/harness-types.js";
|
|
2
2
|
import type { ToolPolicy } from "./tool-policy.js";
|
|
3
|
+
/** design/388 B22 — the identity triple the exemption probe hands the store beside the name. */
|
|
4
|
+
export interface ToolCallIdentity {
|
|
5
|
+
capabilityId: string;
|
|
6
|
+
contractId: string;
|
|
7
|
+
shapeDigest: string;
|
|
8
|
+
}
|
|
3
9
|
/** Options for {@link createFsWriteGatePolicy}. */
|
|
4
10
|
export interface FsWriteGatePolicyOptions {
|
|
5
11
|
/** The task's real execution env (the one the hands run against) — canonicalization must see the
|
|
@@ -37,7 +43,7 @@ export interface FsWriteGatePolicyOptions {
|
|
|
37
43
|
* `combinePolicies` outranks this `allow` under the deny > ask > allow fold, and the two fail-closed
|
|
38
44
|
* asks for an UNRESOLVABLE target fire before any canonical key exists, so they are never exempted.
|
|
39
45
|
*/
|
|
40
|
-
isExempt?: (toolName: string, canonicalPath: string) => boolean | Promise<boolean>;
|
|
46
|
+
isExempt?: (toolName: string, canonicalPath: string, identity?: ToolCallIdentity) => boolean | Promise<boolean>;
|
|
41
47
|
/**
|
|
42
48
|
* design/380 O13 — the deployment HARD-CLOSE knob for guarded writes. Armed (`true`), a covered
|
|
43
49
|
* write whose env does NOT provide `writeFileGuarded` is REFUSED (deny, typed message) — never the
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { canonicalizeTarget, writeTargetPath } from "../tools/fs/safety.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isWithin } from "./runner/session-rule-policy.js";
|
|
3
|
+
import { declaredPathTargetOf, isProtectedWrite } from "./tool-registry.js";
|
|
3
4
|
const ask = (message) => ({ action: "ask", message, decisionReason: "rule" });
|
|
4
5
|
export function createFsWriteGatePolicy(opts) {
|
|
5
6
|
const { env, rootPath, defaultWrite } = opts;
|
|
6
|
-
const gated = PATH_CONFINABLE_WRITE_TOOLS;
|
|
7
7
|
const acceptDirs = opts.acceptDirs && opts.acceptDirs.length > 0 ? opts.acceptDirs : undefined;
|
|
8
8
|
const exemptDirs = opts.exemptDirs && opts.exemptDirs.length > 0 ? opts.exemptDirs : undefined;
|
|
9
9
|
const guardedCapable = env.writeFileGuarded !== undefined;
|
|
@@ -12,8 +12,7 @@ export function createFsWriteGatePolicy(opts) {
|
|
|
12
12
|
: " (advisory adjudication: this environment provides no guarded write, so approval is judged before a separate, non-atomic write step)";
|
|
13
13
|
return {
|
|
14
14
|
async check(req, signal) {
|
|
15
|
-
|
|
16
|
-
if (!gated.has(canonical))
|
|
15
|
+
if (!isProtectedWrite(req))
|
|
17
16
|
return { action: "allow" };
|
|
18
17
|
if (opts.requireGuardedWrite === true && !guardedCapable) {
|
|
19
18
|
return {
|
|
@@ -22,9 +21,10 @@ export function createFsWriteGatePolicy(opts) {
|
|
|
22
21
|
decisionReason: "rule",
|
|
23
22
|
};
|
|
24
23
|
}
|
|
25
|
-
const
|
|
24
|
+
const declared = declaredPathTargetOf(req);
|
|
25
|
+
const path = declared === undefined ? undefined : writeTargetPath(req, declared);
|
|
26
26
|
if (path === undefined) {
|
|
27
|
-
return ask(`write tool "${req.toolName}" requires approval: the call has no resolvable path target to confine${advisory}`);
|
|
27
|
+
return ask(`write tool "${req.toolName}" requires approval: the call has no resolvable path target to confine${declared === undefined ? " (the tool declares no path target)" : ""}${advisory}`);
|
|
28
28
|
}
|
|
29
29
|
const canon = await canonicalizeTarget(env, path, signal, req.cwd ?? rootPath);
|
|
30
30
|
if (!canon.ok) {
|
|
@@ -42,7 +42,7 @@ export function createFsWriteGatePolicy(opts) {
|
|
|
42
42
|
if (opts.isExempt) {
|
|
43
43
|
let exempt = false;
|
|
44
44
|
try {
|
|
45
|
-
exempt = (await opts.isExempt(
|
|
45
|
+
exempt = (await opts.isExempt(req.toolName, canon.key, req.face !== undefined ? { capabilityId: req.face.capabilityId, contractId: req.face.contractId, shapeDigest: req.face.shapeDigest } : undefined)) === true;
|
|
46
46
|
}
|
|
47
47
|
catch {
|
|
48
48
|
exempt = false;
|
package/dist/core/gate-fold.js
CHANGED
|
@@ -30,7 +30,7 @@ function withProbeTimeout(p, ms, signal) {
|
|
|
30
30
|
const DEFAULT_PROBE_TIMEOUT_MS = 30_000;
|
|
31
31
|
export async function runGateFold(pass) {
|
|
32
32
|
const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass;
|
|
33
|
-
pass.req = { toolName, args: pass.currentInput, toolCallId };
|
|
33
|
+
pass.req = { toolName, args: pass.currentInput, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
34
34
|
pass.decision = adjudicate ? await (callSignal !== undefined ? adjudicate(pass.req, callSignal) : adjudicate(pass.req)) : { action: "allow" };
|
|
35
35
|
{
|
|
36
36
|
const refusedBy = ledger.fromDecision(pass.decision);
|
|
@@ -144,7 +144,7 @@ export async function runGateFold(pass) {
|
|
|
144
144
|
pass.tightenedBy = "safety";
|
|
145
145
|
}
|
|
146
146
|
if (pass.decision.action === "allow" && input.writeProtectionCheck !== undefined) {
|
|
147
|
-
const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput);
|
|
147
|
+
const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face);
|
|
148
148
|
if (writeProtectedHit !== null) {
|
|
149
149
|
pass.decision = {
|
|
150
150
|
action: "ask",
|
package/dist/core/gate-lanes.js
CHANGED
|
@@ -548,7 +548,8 @@ export async function runGateLanes(pass) {
|
|
|
548
548
|
let recheck = { action: "allow" };
|
|
549
549
|
if (adjudicate) {
|
|
550
550
|
try {
|
|
551
|
-
|
|
551
|
+
const editReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
552
|
+
recheck = await (callSignal !== undefined ? adjudicate(editReq, callSignal) : adjudicate(editReq));
|
|
552
553
|
}
|
|
553
554
|
catch (err) {
|
|
554
555
|
recheck = {
|
|
@@ -588,7 +589,7 @@ export async function runGateLanes(pass) {
|
|
|
588
589
|
}
|
|
589
590
|
const editAskSnapshot = { ...recheck, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
|
|
590
591
|
const editAskDecision = { ...editAskSnapshot, origin: askOriginOf(editAskSnapshot, askOriginFacts(editOrg.org)) };
|
|
591
|
-
const editAskReq = { toolName, args: editArgs, toolCallId };
|
|
592
|
+
const editAskReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
|
|
592
593
|
const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
|
|
593
594
|
ledger.fromResolved(rr, editAskDecision, toolName);
|
|
594
595
|
if (rr.action !== "allow") {
|
|
@@ -606,7 +607,7 @@ export async function runGateLanes(pass) {
|
|
|
606
607
|
editRewrittenSinceHuman = false;
|
|
607
608
|
}
|
|
608
609
|
if (editDenied === undefined && editRewrittenSinceHuman && input.writeProtectionCheck !== undefined) {
|
|
609
|
-
const editHit = input.writeProtectionCheck(toolName, editArgs);
|
|
610
|
+
const editHit = input.writeProtectionCheck(toolName, editArgs, pass.req.face);
|
|
610
611
|
if (editHit !== null) {
|
|
611
612
|
editDenied = {
|
|
612
613
|
action: "deny",
|
|
@@ -103,7 +103,7 @@ export type NoticeAudience = "user" | "operator";
|
|
|
103
103
|
* src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
|
|
104
104
|
* no longer minted.
|
|
105
105
|
*/
|
|
106
|
-
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
106
|
+
export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
|
|
107
107
|
/** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
|
|
108
108
|
* `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
|
|
109
109
|
* sink is a supported shape, and narrowing that field would break it. */
|
|
@@ -101,6 +101,9 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
101
101
|
"config.tool_model_gate_removed",
|
|
102
102
|
"config.tool_model_gate_unknown_class",
|
|
103
103
|
"config.tool_model_gate_env_invalid",
|
|
104
|
+
"config.tool_card_undeclared",
|
|
105
|
+
"config.tool_face_undeclared",
|
|
106
|
+
"config.tool_face_invalid",
|
|
104
107
|
"config.durable_gate_unavailable",
|
|
105
108
|
"config.peer_admission_out_of_range",
|
|
106
109
|
"config.peer_lane_unmounted",
|
|
@@ -108,6 +111,7 @@ export const ENGINE_NOTICE_CODES = [
|
|
|
108
111
|
"peer.held_settled",
|
|
109
112
|
"peer.idle_subscription",
|
|
110
113
|
"classifier.denial_limit",
|
|
114
|
+
"checkpoint.execution_outcome_unrecorded",
|
|
111
115
|
"delegation.transcript_integrity",
|
|
112
116
|
"mcp.revocation_probe_failed",
|
|
113
117
|
"workflow.governance_key_stripped",
|
|
@@ -163,12 +167,16 @@ const NOTICE_AUDIENCE_TABLE = {
|
|
|
163
167
|
"config.tool_model_gate_removed": "operator",
|
|
164
168
|
"config.tool_model_gate_unknown_class": "operator",
|
|
165
169
|
"config.tool_model_gate_env_invalid": "operator",
|
|
170
|
+
"config.tool_card_undeclared": "operator",
|
|
171
|
+
"config.tool_face_undeclared": "operator",
|
|
172
|
+
"config.tool_face_invalid": "operator",
|
|
166
173
|
"config.peer_lane_unmounted": "operator",
|
|
167
174
|
"config.peer_admission_out_of_range": "operator",
|
|
168
175
|
"peer.inbound_disposition": "user",
|
|
169
176
|
"peer.held_settled": "user",
|
|
170
177
|
"peer.idle_subscription": "user",
|
|
171
178
|
"delegation.transcript_integrity": "operator",
|
|
179
|
+
"checkpoint.execution_outcome_unrecorded": "operator",
|
|
172
180
|
"mcp.revocation_probe_failed": "operator",
|
|
173
181
|
"workflow.governance_key_stripped": "operator",
|
|
174
182
|
"workflow.agent_option_ignored": "operator",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ActorAssertion, DocumentContent, ImageContent, TextContent } from "../internal/llm.js";
|
|
2
2
|
import type { ExecutionEnv, FileError, Result, SessionTreeEntry } from "../internal/harness-types.js";
|
|
3
|
-
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
3
|
+
import type { DecisionReason, PermissionResult, ResolvedAsk, ToolCallFace, ToolCallRequest, ToolPolicy } from "./tool-policy.js";
|
|
4
4
|
import type { GateOutcome } from "./gate-outcome.js";
|
|
5
5
|
export { normalizeOrgGateVerdict, normalizePersistedRuleHit, persistedRuleMandateOf } from "./gate-lanes.js";
|
|
6
6
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
@@ -1115,6 +1115,12 @@ export interface ToolGateInput {
|
|
|
1115
1115
|
* builds ({@link HookEnvCapabilities}). Built ONCE per task by the runner (after the env is minted) and
|
|
1116
1116
|
* passed through unchanged; omitted when the deployment wired no execution environment. */
|
|
1117
1117
|
hookEnv?: HookEnvCapabilities;
|
|
1118
|
+
/**
|
|
1119
|
+
* design/388 B6 — the leg's live roster lookup: the face of the mounted tool a call dispatches to, stamped
|
|
1120
|
+
* on the {@link ToolCallRequest} every policy sees (read at the gate entry, so a run-time roster change is
|
|
1121
|
+
* honoured on the next call). Absent ⇒ requests carry no face (a bare producer).
|
|
1122
|
+
*/
|
|
1123
|
+
toolFaceOf?: (toolName: string) => ToolCallFace | undefined;
|
|
1118
1124
|
/**
|
|
1119
1125
|
* design/384 slice 1 — the PER-CALL abort signal, carried in from the `tool_call` hook event's
|
|
1120
1126
|
* own `signal` seat (the loop's turn-scoped controller; the run abort is composed into it). The
|
|
@@ -1332,7 +1338,7 @@ export interface ToolGateInput {
|
|
|
1332
1338
|
* (the write-protection tighten below). Absent ⇒ the deployment replaced the table with `[]` (or
|
|
1333
1339
|
* the caller runs the gate without one) and the decision path is byte-identical.
|
|
1334
1340
|
*/
|
|
1335
|
-
writeProtectionCheck?: (toolName: string, args: unknown) => import("./write-protect.js").WriteProtectedHit | null;
|
|
1341
|
+
writeProtectionCheck?: (toolName: string, args: unknown, face?: import("./tool-registry.js").ToolCallLike["face"]) => import("./write-protect.js").WriteProtectedHit | null;
|
|
1336
1342
|
/**
|
|
1337
1343
|
* design/143 批2 (A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
|
|
1338
1344
|
* small-model policy CLASSIFIER before any human/durable resolution:
|
package/dist/core/hooks.js
CHANGED
|
@@ -301,7 +301,7 @@ export async function runToolGate(input) {
|
|
|
301
301
|
screening,
|
|
302
302
|
afterFold: runGateLanes,
|
|
303
303
|
currentInput: modelInput,
|
|
304
|
-
req: { toolName, args: modelInput, toolCallId },
|
|
304
|
+
req: { toolName, args: modelInput, toolCallId, ...(() => { const f = input.toolFaceOf?.(toolName); return f !== undefined ? { face: f } : {}; })() },
|
|
305
305
|
preToolContext: [],
|
|
306
306
|
hookAsk: undefined,
|
|
307
307
|
parkFailed: undefined,
|
package/dist/core/lsp.js
CHANGED
|
@@ -2,6 +2,7 @@ import { uriToPath } from "./lsp-protocol.js";
|
|
|
2
2
|
import { foldRedundantPathSegments, isAbsolutePathForm } from "../tools/fs/safety.js";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { defineTool, errorResult } from "./tools.js";
|
|
5
|
+
import { toolFace } from "./tool-catalog-entries.js";
|
|
5
6
|
export const LSP_OPERATIONS = [
|
|
6
7
|
"goToDefinition", "findReferences", "hover", "documentSymbol", "workspaceSymbol",
|
|
7
8
|
"goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls",
|
|
@@ -145,7 +146,7 @@ const POSITION_OPS = new Set([
|
|
|
145
146
|
export function createLspTool(manager, opts = {}) {
|
|
146
147
|
return defineTool({
|
|
147
148
|
name: "LSP",
|
|
148
|
-
|
|
149
|
+
...toolFace("lsp"),
|
|
149
150
|
description: "Interact with Language Server Protocol (LSP) servers to get code intelligence features.\n" +
|
|
150
151
|
"\n" +
|
|
151
152
|
"Supported operations:\n" +
|
|
@@ -172,7 +173,6 @@ export function createLspTool(manager, opts = {}) {
|
|
|
172
173
|
character: Type.Optional(Type.Number({ description: "The character offset (1-based, as shown in editors)." })),
|
|
173
174
|
query: Type.Optional(Type.String({ description: "Symbol query for workspaceSymbol (may be empty to browse broadly)." })),
|
|
174
175
|
}),
|
|
175
|
-
effect: "read",
|
|
176
176
|
execute: async (args, ctx) => {
|
|
177
177
|
const { operation, filePath, line, character, query } = args;
|
|
178
178
|
if (POSITION_OPS.has(operation) && (line === undefined || character === undefined)) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* a longer name. Layer 0 vocabulary; `types.ts` re-exports every name below.
|
|
7
7
|
*/
|
|
8
8
|
import type { ToolContentOrigin, ToolEffect } from "./tool-spec.js";
|
|
9
|
+
import type { ToolFamily, ToolPathTarget, ToolRenderHints, ToolRuleFace } from "./tool-face.js";
|
|
9
10
|
/** Definition of an MCP server to connect to for the duration of one task, then discard. */
|
|
10
11
|
export interface McpServerSpec {
|
|
11
12
|
/** Stable name; tool names are namespaced as `<name>__<tool>` to avoid collisions. */
|
|
@@ -71,6 +72,22 @@ export interface McpServerSpec {
|
|
|
71
72
|
egress?: boolean;
|
|
72
73
|
irreversibility?: "always" | "never";
|
|
73
74
|
}>;
|
|
75
|
+
/**
|
|
76
|
+
* design/388 §1.1 / §2.7 — the OPTIONAL per-tool FACE overlay for this server's tools (keyed by the server's
|
|
77
|
+
* RAW tool name, like {@link allowTools} / {@link toolAxes}): a deployment lets an MCP tool enter a specialised
|
|
78
|
+
* approval card, be confirmed by path, carry a display name / summary, or narrow its rule vocabulary. TRUSTED
|
|
79
|
+
* (the deployment writes it), but validated against the MATERIALIZED schema at every listing (first mount and
|
|
80
|
+
* every refresh): a `pathTarget.param` that is not a top-level property, an `approvalCard` whose keys the schema
|
|
81
|
+
* lacks, a `ruleFace` naming a non-scalar key ⇒ the tool mounts WITHOUT the overlay and the leg announces
|
|
82
|
+
* `config.tool_face_invalid` once; a key naming a tool the listing does not carry announces
|
|
83
|
+
* `config.tool_face_undeclared` once. Applied whole or not at all.
|
|
84
|
+
*/
|
|
85
|
+
toolFaces?: Record<string, {
|
|
86
|
+
family?: ToolFamily;
|
|
87
|
+
pathTarget?: ToolPathTarget;
|
|
88
|
+
renderHints?: ToolRenderHints;
|
|
89
|
+
ruleFace?: ToolRuleFace;
|
|
90
|
+
}>;
|
|
74
91
|
/**
|
|
75
92
|
* design/378 — declare {@link ToolSpec.contentOrigin} on behalf of THIS SERVER'S ENTIRE TOOL SET
|
|
76
93
|
* (tools a mid-task refresh adds included), with the same authority and the same responsibility a
|
package/dist/core/mcp.js
CHANGED
|
@@ -18,6 +18,7 @@ import { discloseReminderShaped, observeReminderMarkEcho } from "./reminder-disc
|
|
|
18
18
|
import { withContentOrigin } from "./memory-engine/content-origin.js";
|
|
19
19
|
import { snapshotMcpEntries } from "./locked-config.js";
|
|
20
20
|
import { validateJsonSchemaShape } from "./runner/strict-output-schema.js";
|
|
21
|
+
import { schemaKeyBoundProblem } from "./tool-roster.js";
|
|
21
22
|
export const MCP_PREFIX = MCP_NAMESPACE.prefix;
|
|
22
23
|
const MCP_OUTPUT_TOKENS_DEFAULT = 25_000;
|
|
23
24
|
const MCP_CHARS_PER_TOKEN = 4;
|
|
@@ -1513,7 +1514,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1513
1514
|
effectiveInputSchema = normalized.schema;
|
|
1514
1515
|
effectiveDescription = normalized.note + (effectiveDescription ? `\n\n${effectiveDescription}` : "");
|
|
1515
1516
|
}
|
|
1516
|
-
const schemaProblem = mcpToolSchemaProblem(effectiveInputSchema);
|
|
1517
|
+
const schemaProblem = mcpToolSchemaProblem(effectiveInputSchema) ?? schemaKeyBoundProblem(effectiveInputSchema);
|
|
1517
1518
|
if (schemaProblem !== undefined) {
|
|
1518
1519
|
dropped.push({ tool: inlineUntrusted(t.name), reason: inlineUntrusted(schemaProblem, 240) });
|
|
1519
1520
|
continue;
|
|
@@ -1551,6 +1552,7 @@ function intakeListedTools(listed, spec, client, health, imageResizer, reminderD
|
|
|
1551
1552
|
parameters: (effectiveInputSchema ?? { type: "object" }),
|
|
1552
1553
|
...(mcpMaxResultSizeChars !== undefined ? { mcpMaxResultSizeChars } : {}),
|
|
1553
1554
|
...(mcpAlwaysLoad ? { mcpAlwaysLoad: true } : {}),
|
|
1555
|
+
mcpRemoteName: remoteName,
|
|
1554
1556
|
execute: async (_toolCallId, params, signal) => {
|
|
1555
1557
|
const what = `The call to tool ${inlineUntrusted(remoteName)}`;
|
|
1556
1558
|
if (isServerRevoked?.(spec.name) === true) {
|
|
@@ -6,6 +6,7 @@ import { formatMemoryAge } from "../memory-recall.js";
|
|
|
6
6
|
import { committedOriginOf } from "./frontmatter.js";
|
|
7
7
|
import { MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, MEMORY_PROVENANCE_INDEX_SENTENCE, MEMORY_PROVENANCE_SEARCH_SENTENCE, MEMORY_SEARCH_BAND_ORDER_HEADER } from "./provenance-wording.js";
|
|
8
8
|
import { deriveSupersededSet, memorySupersededNote, MEMORY_SEARCH_SUPERSEDED_TAG } from "./consolidation.js";
|
|
9
|
+
import { toolFace } from "../tool-catalog-entries.js";
|
|
9
10
|
export const MEMORY_SEARCH_TOOL_NAME = "memory_search";
|
|
10
11
|
export const MEMORY_GET_TOOL_NAME = "memory_get";
|
|
11
12
|
export const MEMORY_INDEX_TOOL_NAME = "memory_index";
|
|
@@ -175,11 +176,8 @@ export function createMemoryEngineTools(opts) {
|
|
|
175
176
|
};
|
|
176
177
|
const searchTool = {
|
|
177
178
|
name: MEMORY_SEARCH_TOOL_NAME,
|
|
179
|
+
...toolFace("memory-search"),
|
|
178
180
|
description: carry ? `${SEARCH_DESCRIPTION}\n\n${MEMORY_PROVENANCE_SEARCH_SENTENCE}` : SEARCH_DESCRIPTION,
|
|
179
|
-
effect: "read",
|
|
180
|
-
offload: false,
|
|
181
|
-
contentOrigin: "local",
|
|
182
|
-
contract: { contractId: "core.memory_search@1", implementationRevision: "6" },
|
|
183
181
|
parameters: Type.Object({
|
|
184
182
|
query: Type.String({ description: "Keywords to look for (lexical match against entry names, descriptions and bodies)." }),
|
|
185
183
|
limit: Type.Optional(Type.Number({ description: `Maximum hits to return (default ${MEMORY_SEARCH_DEFAULT_LIMIT}, max ${MEMORY_SEARCH_MAX_LIMIT}).` })),
|
|
@@ -319,11 +317,8 @@ export function createMemoryEngineTools(opts) {
|
|
|
319
317
|
};
|
|
320
318
|
const getTool = {
|
|
321
319
|
name: MEMORY_GET_TOOL_NAME,
|
|
320
|
+
...toolFace("memory-get"),
|
|
322
321
|
description: GET_DESCRIPTION,
|
|
323
|
-
effect: "read",
|
|
324
|
-
offload: false,
|
|
325
|
-
contentOrigin: "local",
|
|
326
|
-
contract: { contractId: "core.memory_get@1", implementationRevision: "6" },
|
|
327
322
|
parameters: Type.Object({
|
|
328
323
|
id: Type.Optional(Type.String({ description: "Entry id (exact lookup). Pass either id or slug, not both." })),
|
|
329
324
|
slug: Type.Optional(Type.String({ description: "Entry slug (its file path without .md). Ambiguous across scopes unless scope is also passed." })),
|
|
@@ -545,11 +540,8 @@ export function createMemoryEngineTools(opts) {
|
|
|
545
540
|
};
|
|
546
541
|
const indexTool = {
|
|
547
542
|
name: MEMORY_INDEX_TOOL_NAME,
|
|
543
|
+
...toolFace("memory-index"),
|
|
548
544
|
description: carry ? `${INDEX_DESCRIPTION}\n\n${MEMORY_PROVENANCE_INDEX_SENTENCE}` : INDEX_DESCRIPTION,
|
|
549
|
-
effect: "read",
|
|
550
|
-
offload: false,
|
|
551
|
-
contentOrigin: "local",
|
|
552
|
-
contract: { contractId: "core.memory_index@1", implementationRevision: "1" },
|
|
553
545
|
parameters: Type.Object({
|
|
554
546
|
offset: Type.Optional(Type.Number({ description: "Zero-based row offset into the listing (default 0) — pass the previous page's footer value to continue." })),
|
|
555
547
|
limit: Type.Optional(Type.Number({ description: `Maximum rows for this page (default ${MEMORY_INDEX_PAGE_ROWS}, max ${MEMORY_INDEX_MAX_PAGE_ROWS}).` })),
|
|
@@ -43,6 +43,11 @@ export interface PermissionRuleIssue {
|
|
|
43
43
|
/** `invalid.*` = 语法/语义错;`unsupported.*` = CC 合法但 v1 不支持的 lane(独立
|
|
44
44
|
* 分类,绝不静默 no-op —— `Bash(npm run:*)` 被当 generic 参数规则接受后永不匹配是最危险的失败形态)。 */
|
|
45
45
|
code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix"
|
|
46
|
+
/** design/388 B15 — the closed rule-lint set over a tool's RULE FACE (derived scalar vocabulary; a
|
|
47
|
+
* declaration only narrows it): the named parameter is not one the tool carries; it is carried but
|
|
48
|
+
* is an object/array (a rule on it can never match); the tool is unknown to the resolver a leg
|
|
49
|
+
* supplied (absent resolver ⇒ the catalog alone judges and an unknown tool stays permissive). */
|
|
50
|
+
| "unsupported.param_unknown" | "unsupported.param_non_scalar" | "unsupported.tool_unknown"
|
|
46
51
|
/** design/179 D7: a Bash prefix rule under `bashPrefixLane:"rules-store"` — reported so a migration
|
|
47
52
|
* report can tell "route this through the rule store" apart from "this is a mistake". */
|
|
48
53
|
| "unsupported.bash_prefix_rules_store" | "unsupported.file_glob" | "unsupported.mcp_paren"
|
|
@@ -98,6 +103,13 @@ export interface PermissionRulePolicyOptions {
|
|
|
98
103
|
*/
|
|
99
104
|
bashPrefixLane?: "reject" | "rules-store";
|
|
100
105
|
caps?: Partial<PermissionRuleCaps>;
|
|
106
|
+
/**
|
|
107
|
+
* design/388 B15 — the leg's RULE-FACE resolver (typically `ruleFacesFromRoster(prepared.toolRoster)`): with it
|
|
108
|
+
* every tool on the leg is judged — MCP and caller tools included — and a rule naming a tool the leg does not
|
|
109
|
+
* mount is `unsupported.tool_unknown`. Without it the static catalog alone judges; a tool the catalog does not
|
|
110
|
+
* carry stays permissive (a rule for it compiles unjudged).
|
|
111
|
+
*/
|
|
112
|
+
ruleFaces?: (toolName: string) => RuleFaceView | undefined;
|
|
101
113
|
}
|
|
102
114
|
/**
|
|
103
115
|
* Parse one rule string — CC `Lh` :55214 parity, including its lenient downgrades (malformed parens /
|
|
@@ -134,11 +146,29 @@ export declare function namespacedRuleNameCovers(ruleName: string, toolName: str
|
|
|
134
146
|
* — i.e. the shapes {@link namespacedRuleNameCovers} answers and an exact name table cannot. Every
|
|
135
147
|
* lane that keys rules by exact tool name needs this to know which entries it must NOT key that way. */
|
|
136
148
|
export declare function isNamespacedCoveringRuleName(name: string): boolean;
|
|
137
|
-
/**
|
|
149
|
+
/**
|
|
150
|
+
* design/388 A-6/7/8/9 (D-3): the per-tool RULE FACE — the parameters a `Tool(param:pattern)` rule may name
|
|
151
|
+
* (`params`, the SCALAR top-level keys ∪ the path target's aliases), the ones the generic lexical matcher must
|
|
152
|
+
* not be pointed at (`primaryParams`), the carried-but-non-scalar keys a rule can never match (`nonScalar`), and
|
|
153
|
+
* whether the tool names a path target at all (the file-tool glob lane). Resolved through
|
|
154
|
+
* {@link PermissionRulePolicyOptions.ruleFaces} when a leg supplies its roster, else from the static catalog
|
|
155
|
+
* — the three hand-kept tables (`PRIMARY_FIELDS` / `FILE_TOOLS` / `FILE_TOOL_PARAMS`) this replaced are gone.
|
|
156
|
+
*/
|
|
157
|
+
export interface RuleFaceView {
|
|
158
|
+
primaryParams: readonly string[];
|
|
159
|
+
/** The full scalar vocabulary; ABSENT when the resolver does not know it (the static catalog for a
|
|
160
|
+
* definition whose schema is factory-built) — then only the primary-field check applies. */
|
|
161
|
+
params?: readonly string[];
|
|
162
|
+
nonScalar?: readonly string[];
|
|
163
|
+
pathTargeting?: boolean;
|
|
164
|
+
}
|
|
165
|
+
/** The static catalog's answer for a wire name (undefined ⇒ the catalog does not carry it). */
|
|
166
|
+
export declare function catalogRuleFaceView(toolName: string): RuleFaceView | undefined;
|
|
167
|
+
/** Exported for the lockstep guard only (the scalar vocabulary of a catalog tool) — not part of the rule DSL. */
|
|
138
168
|
export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
|
|
139
169
|
/** Dry-run 校验/迁移报告:不 throw,返回全部 issues(含 `unsupported.*` 分类,
|
|
140
170
|
* 供从 CC settings 迁移的部署逐条对照)。 */
|
|
141
|
-
export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps" | "bashPrefixLane">): PermissionRuleIssue[];
|
|
171
|
+
export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps" | "bashPrefixLane" | "ruleFaces">): PermissionRuleIssue[];
|
|
142
172
|
/**
|
|
143
173
|
* Compile declarative permission rules into ONE {@link ToolPolicy} (design/127). Evaluation order
|
|
144
174
|
* inside the policy (CC `G1e` :595272 isomorph): bare deny → param deny → bare ask → param ask →
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
|
|
2
|
-
|
|
2
|
+
import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
|
|
3
|
+
export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
|
|
3
4
|
const DEFAULT_CAPS = {
|
|
4
5
|
maxRules: 256,
|
|
5
6
|
maxRuleLength: 512,
|
|
@@ -107,31 +108,16 @@ export function isNamespacedCoveringRuleName(name) {
|
|
|
107
108
|
const parsed = parseNamespacedRuleName(name);
|
|
108
109
|
return parsed !== undefined && (parsed.tool === undefined || parsed.tool.includes("*"));
|
|
109
110
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
WebFetch: new Set(["url"]),
|
|
120
|
-
WebSearch: new Set(["query"]),
|
|
121
|
-
};
|
|
122
|
-
const FILE_TOOLS = new Set(["Read", "Edit", "Write", "MultiEdit", "NotebookEdit", "Grep", "Glob"]);
|
|
123
|
-
const FILE_TOOL_PARAMS = {
|
|
124
|
-
Read: new Set(["file_path", "path", "offset", "limit", "pages"]),
|
|
125
|
-
Edit: new Set(["file_path", "path", "old_string", "new_string", "replace_all"]),
|
|
126
|
-
Write: new Set(["file_path", "path", "content"]),
|
|
127
|
-
NotebookEdit: new Set(["notebook_path", "path", "cell_id", "cell_type", "edit_mode", "new_source"]),
|
|
128
|
-
Grep: new Set([
|
|
129
|
-
"pattern", "path", "glob", "type", "output_mode", "head_limit", "offset",
|
|
130
|
-
"multiline", "context", "ignore_case", "-i", "-n", "-o", "-A", "-B", "-C",
|
|
131
|
-
]),
|
|
132
|
-
Glob: new Set(["pattern", "path", "max_results"]),
|
|
111
|
+
export function catalogRuleFaceView(toolName) {
|
|
112
|
+
const face = catalogRuleFaceOf(toolName);
|
|
113
|
+
if (face === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
return { ...face, pathTargeting: pathTargetOf(toolName) !== undefined };
|
|
116
|
+
}
|
|
117
|
+
export const fileToolParamVocabulary = (canonicalTool) => {
|
|
118
|
+
const face = catalogRuleFaceOf(canonicalTool);
|
|
119
|
+
return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
|
|
133
120
|
};
|
|
134
|
-
export const fileToolParamVocabulary = (canonicalTool) => FILE_TOOL_PARAMS[canonicalTool];
|
|
135
121
|
function countStars(s) {
|
|
136
122
|
let n = 0;
|
|
137
123
|
for (const c of s)
|
|
@@ -139,7 +125,15 @@ function countStars(s) {
|
|
|
139
125
|
n++;
|
|
140
126
|
return n;
|
|
141
127
|
}
|
|
142
|
-
function compile(rules, caps, primaryFieldGeneric, bashPrefixLane = "reject") {
|
|
128
|
+
function compile(rules, caps, primaryFieldGeneric, bashPrefixLane = "reject", ruleFaces) {
|
|
129
|
+
const faceOf = (toolName) => {
|
|
130
|
+
if (ruleFaces !== undefined) {
|
|
131
|
+
const face = ruleFaces(toolName);
|
|
132
|
+
return { face, judged: true };
|
|
133
|
+
}
|
|
134
|
+
const face = catalogRuleFaceView(toolName);
|
|
135
|
+
return { face, judged: face !== undefined };
|
|
136
|
+
};
|
|
143
137
|
const byTool = new Map();
|
|
144
138
|
const namespacedCovering = { deny: [], ask: [], allow: [] };
|
|
145
139
|
const issues = [];
|
|
@@ -209,18 +203,30 @@ function compile(rules, caps, primaryFieldGeneric, bashPrefixLane = "reject") {
|
|
|
209
203
|
continue;
|
|
210
204
|
}
|
|
211
205
|
}
|
|
212
|
-
else
|
|
213
|
-
|
|
214
|
-
|
|
206
|
+
else {
|
|
207
|
+
const resolved = faceOf(toolName);
|
|
208
|
+
if (resolved.judged && resolved.face === undefined) {
|
|
209
|
+
bad(text, "unsupported.tool_unknown", `"${toolName}" is not a tool on this leg's roster — the rule would compile and never match`);
|
|
215
210
|
continue;
|
|
216
211
|
}
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
bad(text, "unsupported.unknown_param", `"${paramPart}" is not a parameter of ${toolName} (it carries ${[...vocabulary].sort().join(", ")}) — the rule would compile and never match. ` +
|
|
221
|
-
`For a path glob use the tool-specific lane (\`${toolName}(src/**)\` is not the generic param DSL); a Windows path such as \`${toolName}(C:/foo)\` parses as the param "C".`);
|
|
212
|
+
const face = resolved.face;
|
|
213
|
+
if (face !== undefined && face.pathTargeting === true && colon <= 0) {
|
|
214
|
+
bad(text, "unsupported.file_glob", "file-tool glob rules (`Edit(src/**)`) are the tool-specific lane — not supported by the generic param DSL");
|
|
222
215
|
continue;
|
|
223
216
|
}
|
|
217
|
+
if (face?.params !== undefined && colon > 0) {
|
|
218
|
+
const paramPart = content.slice(0, colon).trim();
|
|
219
|
+
if (!face.params.includes(paramPart)) {
|
|
220
|
+
if (face.nonScalar?.includes(paramPart)) {
|
|
221
|
+
bad(text, "unsupported.param_non_scalar", `"${paramPart}" is a parameter of ${toolName} but its value is an object or an array — the generic matcher compares scalars only; the rule would compile and never match`);
|
|
222
|
+
}
|
|
223
|
+
else {
|
|
224
|
+
bad(text, "unsupported.param_unknown", `"${paramPart}" is not a parameter of ${toolName} (it carries ${[...face.params].sort().join(", ")}) — the rule would compile and never match. ` +
|
|
225
|
+
`For a path glob use the tool-specific lane (\`${toolName}(src/**)\` is not the generic param DSL); a Windows path such as \`${toolName}(C:/foo)\` parses as the param "C".`);
|
|
226
|
+
}
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
224
230
|
}
|
|
225
231
|
if (colon <= 0) {
|
|
226
232
|
bad(text, "invalid.param_split", "param-level ruleContent must be `param:pattern`");
|
|
@@ -240,7 +246,7 @@ function compile(rules, caps, primaryFieldGeneric, bashPrefixLane = "reject") {
|
|
|
240
246
|
bad(text, "invalid.cap_exceeded", `pattern exceeds maxPatternLength ${caps.maxPatternLength} or maxStars ${caps.maxStars}`);
|
|
241
247
|
continue;
|
|
242
248
|
}
|
|
243
|
-
if (
|
|
249
|
+
if (faceOf(toolName).face?.primaryParams.includes(param) === true && primaryFieldGeneric === "reject") {
|
|
244
250
|
bad(text, "invalid.primary_field", `"${param}" is ${toolName}'s primary field — the generic matcher is a trim+FULL-STRING lexical match (no shell/path semantics); '${toolName}(${param}:git push*)' would NOT match 'cd /x && git push'. Opt in with primaryFieldGeneric:"allow-lexical" if you understand this.`);
|
|
245
251
|
continue;
|
|
246
252
|
}
|
|
@@ -251,7 +257,7 @@ function compile(rules, caps, primaryFieldGeneric, bashPrefixLane = "reject") {
|
|
|
251
257
|
}
|
|
252
258
|
export function validatePermissionRules(rules, opts) {
|
|
253
259
|
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
|
|
254
|
-
return compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.bashPrefixLane ?? "reject").issues;
|
|
260
|
+
return compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.bashPrefixLane ?? "reject", opts?.ruleFaces).issues;
|
|
255
261
|
}
|
|
256
262
|
function isPlainObjectArgs(args) {
|
|
257
263
|
return typeof args === "object" && args !== null && !Array.isArray(args);
|
|
@@ -281,7 +287,7 @@ function ruleMessage(kind, ruleText, source) {
|
|
|
281
287
|
}
|
|
282
288
|
export function createPermissionRulePolicy(rules, opts) {
|
|
283
289
|
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
|
|
284
|
-
const { byTool, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.bashPrefixLane ?? "reject");
|
|
290
|
+
const { byTool, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.bashPrefixLane ?? "reject", opts?.ruleFaces);
|
|
285
291
|
const setLevelCap = issues.some((i) => i.code === "invalid.cap_exceeded" && i.rule === "");
|
|
286
292
|
if (issues.length > 0 && ((opts?.onInvalidRule ?? "throw") === "throw" || setLevelCap)) {
|
|
287
293
|
const e = new Error(`createPermissionRulePolicy: ${issues.length} invalid/unsupported rule(s):\n` +
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
3
|
+
import { toolFace } from "./tool-catalog-entries.js";
|
|
3
4
|
export const PRESENT_PLAN_TOOL_NAME = "ExitPlanMode";
|
|
4
5
|
export const ENTER_PLAN_MODE_TOOL_NAME = "EnterPlanMode";
|
|
5
6
|
const ENTER_PLAN_MODE_DESCRIPTION = `Enter PLAN MODE (read-only): stop making changes and switch to exploration/planning. For the rest of this run, write/edit/mutating tool calls are REJECTED while read-only tools (Read, Grep, Glob, etc.) still work. When your plan is ready, call ${PRESENT_PLAN_TOOL_NAME} with it — the run PAUSES for a human to approve it, then continues (read-only lifted) so you can execute.
|
|
@@ -31,10 +32,9 @@ BAD: "Fix the typo in the README" (trivial; nothing to align on). "Remove the le
|
|
|
31
32
|
export function createEnterPlanModeTool(enterPlanMode) {
|
|
32
33
|
return {
|
|
33
34
|
name: ENTER_PLAN_MODE_TOOL_NAME,
|
|
34
|
-
|
|
35
|
+
...toolFace("enter-plan-mode"),
|
|
35
36
|
description: ENTER_PLAN_MODE_DESCRIPTION,
|
|
36
37
|
parameters: Type.Object({}),
|
|
37
|
-
effect: "read",
|
|
38
38
|
executionMode: "sequential",
|
|
39
39
|
execute: async () => {
|
|
40
40
|
enterPlanMode();
|
|
@@ -61,14 +61,13 @@ const PRESENT_PLAN_DESCRIPTION = [
|
|
|
61
61
|
export function createPresentPlanTool(requestReview) {
|
|
62
62
|
return {
|
|
63
63
|
name: PRESENT_PLAN_TOOL_NAME,
|
|
64
|
-
|
|
64
|
+
...toolFace("exit-plan-mode"),
|
|
65
65
|
description: PRESENT_PLAN_DESCRIPTION,
|
|
66
66
|
parameters: Type.Object({
|
|
67
67
|
plan: Type.String({
|
|
68
68
|
description: "Your full proposed plan, in markdown. The human reviewer approves / edits / rejects it before you act.",
|
|
69
69
|
}),
|
|
70
70
|
}),
|
|
71
|
-
effect: "read",
|
|
72
71
|
execute: async (args) => {
|
|
73
72
|
const { plan } = args;
|
|
74
73
|
requestReview({ reason: "ExitPlanMode" });
|