@sema-agent/core 7.33.7 → 7.34.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 +357 -0
- package/dist/agents/cross-session-judge.d.ts +16 -7
- package/dist/agents/cross-session-judge.js +18 -15
- package/dist/agents/peer-session-drain.d.ts +4 -0
- package/dist/agents/peer-session-drain.js +5 -2
- package/dist/agents/repair-loop.d.ts +1 -1
- package/dist/agents/subagent.d.ts +3 -0
- package/dist/agents/subagent.js +12 -3
- package/dist/brain/anthropic.js +1 -0
- package/dist/brain/open-responses.js +1 -0
- package/dist/brain/openai.js +1 -0
- package/dist/brain/stream-engine.d.ts +3 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/core/a2a.d.ts +3 -1
- package/dist/core/a2a.js +1 -1
- package/dist/core/ask-origin.d.ts +351 -154
- package/dist/core/ask-origin.js +223 -45
- package/dist/core/ask-unresolvable-notice.d.ts +5 -2
- package/dist/core/ask-unresolvable-notice.js +4 -3
- package/dist/core/auto-mode-arming.d.ts +17 -4
- package/dist/core/auto-mode-arming.js +31 -6
- package/dist/core/auto-mode-defaults.d.ts +16 -7
- package/dist/core/auto-mode-defaults.js +2 -1
- package/dist/core/auto-mode-directive.d.ts +18 -30
- package/dist/core/auto-mode-directive.js +9 -18
- package/dist/core/auto-mode-prompt.d.ts +62 -1
- package/dist/core/auto-mode-prompt.js +42 -4
- package/dist/core/auto-mode-rebuild.d.ts +2 -1
- package/dist/core/auto-mode-rebuild.js +1 -0
- package/dist/core/auto-mode.d.ts +32 -3
- package/dist/core/auto-mode.js +80 -41
- package/dist/core/checkpoint-store.d.ts +50 -35
- package/dist/core/checkpoint-store.js +58 -3
- package/dist/core/child-clamp-seats.js +1 -0
- package/dist/core/classifier-unavailable-arm.d.ts +57 -0
- package/dist/core/classifier-unavailable-arm.js +17 -0
- package/dist/core/effective-path-target.d.ts +12 -0
- package/dist/core/effective-path-target.js +18 -7
- package/dist/core/engine-notice.d.ts +53 -2
- package/dist/core/fs-write-gate-policy.d.ts +5 -1
- package/dist/core/fs-write-gate-policy.js +4 -1
- package/dist/core/gate-fold.js +17 -1
- package/dist/core/gate-lanes.js +111 -46
- package/dist/core/gate-outcome.d.ts +40 -7
- package/dist/core/gate-outcome.js +26 -4
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +12 -0
- package/dist/core/hooks.d.ts +50 -29
- package/dist/core/hooks.js +1 -3
- package/dist/core/mcp.js +1 -1
- package/dist/core/mode-policy.d.ts +182 -0
- package/dist/core/mode-policy.js +179 -0
- package/dist/core/oracle-isolation.d.ts +3 -3
- package/dist/core/permission-rule-model.d.ts +16 -3
- package/dist/core/permission-rule-model.js +36 -3
- package/dist/core/permission-rule-provider.d.ts +22 -11
- package/dist/core/permission-rule-provider.js +17 -7
- package/dist/core/permission-rules.d.ts +23 -1
- package/dist/core/permission-rules.js +35 -8
- package/dist/core/persisted-rule-arms.d.ts +10 -9
- package/dist/core/persisted-rule-arms.js +9 -6
- package/dist/core/present-plan-tool.d.ts +1 -1
- package/dist/core/present-plan-tool.js +3 -3
- package/dist/core/read-only-shell.d.ts +19 -19
- package/dist/core/read-only-shell.js +38 -12
- package/dist/core/retired-keys.d.ts +9 -3
- package/dist/core/retired-keys.js +5 -1
- package/dist/core/runner/classifier-ladder.d.ts +43 -1
- package/dist/core/runner/classifier-ladder.js +29 -0
- package/dist/core/runner/contracts.d.ts +37 -35
- package/dist/core/runner/denial-limit-arms.d.ts +30 -7
- package/dist/core/runner/denial-limit-arms.js +15 -9
- package/dist/core/runner/env-capability-census.js +1 -1
- package/dist/core/runner/gate-exit.d.ts +30 -4
- package/dist/core/runner/gate-exit.js +28 -7
- package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
- package/dist/core/runner/permission-rule-lanes.js +44 -40
- package/dist/core/runner/persisted-path-rows.d.ts +64 -0
- package/dist/core/runner/persisted-path-rows.js +89 -0
- package/dist/core/runner/prepare-artifact.d.ts +5 -1
- package/dist/core/runner/prepare-artifact.js +9 -3
- package/dist/core/runner/prepare-ask-lane.d.ts +5 -3
- package/dist/core/runner/prepare-ask-lane.js +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
- package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
- package/dist/core/runner/prepare-gate-stations.d.ts +33 -6
- package/dist/core/runner/prepare-gate-stations.js +32 -19
- package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
- package/dist/core/runner/prepare-hands-readface.js +64 -53
- package/dist/core/runner/prepare-inherited-gate.d.ts +22 -22
- package/dist/core/runner/prepare-inherited-gate.js +37 -23
- package/dist/core/runner/prepare-park-ask.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.js +2 -1
- package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
- package/dist/core/runner/prepare-policy-chain.js +39 -12
- package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
- package/dist/core/runner/prepare-protocol-tools.js +13 -3
- package/dist/core/runner/prepare-run-refs.d.ts +6 -6
- package/dist/core/runner/prepare-run-refs.js +9 -4
- package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
- package/dist/core/runner/prepare-safety-scan.js +19 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
- package/dist/core/runner/prepare-suspend-saga.js +6 -6
- package/dist/core/runner/prepare-task.js +51 -22
- package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
- package/dist/core/runner/prepare-wiring-manifest.js +5 -3
- package/dist/core/runner/read-root-grant.d.ts +9 -0
- package/dist/core/runner/read-root-grant.js +12 -0
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-identity-wiring.js +1 -1
- package/dist/core/runner/run-leg.js +24 -15
- package/dist/core/runner/run-notification-lane.js +1 -0
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
- package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
- package/dist/core/runner/run-turn-boundary.js +1 -1
- package/dist/core/runner/runtask.js +3 -0
- package/dist/core/runner/stream-mode-verb.d.ts +27 -0
- package/dist/core/runner/stream-mode-verb.js +19 -0
- package/dist/core/runner/turn-attachments.d.ts +3 -3
- package/dist/core/runner/turn-attachments.js +1 -1
- package/dist/core/runner/write-protection-station.d.ts +7 -1
- package/dist/core/runner/write-protection-station.js +6 -3
- package/dist/core/runner-deps.d.ts +31 -34
- package/dist/core/runtime-caps.d.ts +12 -3
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +1 -1
- package/dist/core/secret-env.d.ts +16 -2
- package/dist/core/secret-env.js +25 -1
- package/dist/core/sensitive-path-policy.d.ts +4 -2
- package/dist/core/sensitive-path-policy.js +39 -30
- package/dist/core/spec-contract.d.ts +15 -0
- package/dist/core/spec-contract.js +19 -10
- package/dist/core/task-event.d.ts +1 -1
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +41 -89
- package/dist/core/task-stream.d.ts +16 -0
- package/dist/core/terminal-cause.js +1 -0
- package/dist/core/tighten-task-spec.d.ts +2 -2
- package/dist/core/tighten-task-spec.js +12 -14
- package/dist/core/tool-catalog-entries.js +4 -3
- package/dist/core/tool-policy.d.ts +54 -25
- package/dist/core/tool-policy.js +43 -2
- package/dist/core/tool-registry.d.ts +2 -0
- package/dist/core/tool-registry.js +3 -0
- package/dist/core/tool-spec.d.ts +8 -9
- package/dist/core/trace.d.ts +7 -6
- package/dist/core/types.d.ts +2 -1
- package/dist/core/wiring-manifest.d.ts +31 -5
- package/dist/core/wiring-manifest.js +28 -1
- package/dist/core/write-protect.d.ts +11 -1
- package/dist/core/write-protect.js +26 -15
- package/dist/engine/execution-env/kill-tree.js +4 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -0
- package/dist/engine/execution-env/node-execution-env.js +34 -35
- package/dist/engine/harness/types.d.ts +6 -7
- package/dist/engine/llm/types.d.ts +2 -4
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/index.d.ts +15 -5
- package/dist/index.js +12 -3
- package/dist/orchestration/run-spec.d.ts +4 -0
- package/dist/orchestration/run-spec.js +4 -3
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-size-guideline.js +2 -2
- package/dist/stores/file/fs-atomic.js +2 -1
- package/dist/tools/artifact/artifact-tool.d.ts +17 -0
- package/dist/tools/artifact/artifact-tool.js +16 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
- package/dist/tools/fs/bash-readonly-classifier.js +88 -0
- package/dist/tools/fs/containment-face.d.ts +9 -3
- package/dist/tools/fs/containment-face.js +2 -0
- package/dist/tools/fs/file-read-check.d.ts +49 -0
- package/dist/tools/fs/file-read-check.js +71 -0
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-read.d.ts +1 -0
- package/dist/tools/fs/fs-search-tools.d.ts +2 -2
- package/dist/tools/fs/fs-search-tools.js +4 -4
- package/dist/tools/fs/fs-shared.js +20 -6
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +39 -3
- package/dist/tools/fs/index.js +38 -15
- package/dist/tools/fs/read-deny-home.d.ts +93 -0
- package/dist/tools/fs/read-deny-home.js +225 -0
- package/dist/tools/fs/read-deny.d.ts +176 -34
- package/dist/tools/fs/read-deny.js +268 -61
- package/dist/tools/fs/safety.d.ts +6 -1
- package/dist/tools/fs/safety.js +18 -5
- package/dist/tools/fs/search.d.ts +3 -0
- package/dist/tools/fs/search.js +47 -9
- package/dist/tools/fs/shell-read-check.d.ts +42 -11
- package/dist/tools/fs/shell-read-check.js +188 -13
- package/dist/tools/fs/shell-read-establish.d.ts +7 -3
- package/dist/tools/fs/shell-read-establish.js +134 -21
- package/dist/tools/fs/shell-read-identity.d.ts +7 -0
- package/dist/tools/fs/shell-read-identity.js +21 -0
- package/dist/tools/fs/shell-read-judge.d.ts +92 -32
- package/dist/tools/fs/shell-read-judge.js +43 -18
- package/dist/tools/fs/shell-read-plan.d.ts +4 -0
- package/dist/tools/fs/shell-read-plan.js +26 -4
- package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
- package/dist/tools/fs/shell-read-write-targets.js +173 -0
- package/package.json +3 -1
- package/test/export-surface.snapshot.json +103 -11
- package/wire-changes/core@7.33.8.json +165 -0
- package/wire-changes/core@7.34.0.json +2621 -0
package/dist/core/tool-policy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
4
|
+
import { decisionAskClassesOf, headlessDenialMessage } from "./ask-origin.js";
|
|
4
5
|
import { ownReadRootCandidate, readRootGrantSeat } from "./read-root-grant-screen.js";
|
|
5
6
|
import { decidersNote, settledByPerson } from "./gate-outcome.js";
|
|
6
7
|
import { sanitizeAutoModeArmingRecipe } from "./auto-mode-arming.js";
|
|
@@ -121,6 +122,26 @@ export const ASK_EVIDENCE_ABSENCE_VALUES = ["not_wired", "not_adjudicated", "una
|
|
|
121
122
|
export function decisionText(d) {
|
|
122
123
|
return d.message;
|
|
123
124
|
}
|
|
125
|
+
export function toolPolicyPathDenyRules(policies) {
|
|
126
|
+
return policies.flatMap((p) => (Array.isArray(p?.pathDenyRules) ? p.pathDenyRules : []));
|
|
127
|
+
}
|
|
128
|
+
export function toolPolicyWriteTargetGuard(policies) {
|
|
129
|
+
const guards = policies.flatMap((p) => (typeof p?.writeTargetGuard === "function" ? [p.writeTargetGuard] : []));
|
|
130
|
+
if (guards.length === 0)
|
|
131
|
+
return undefined;
|
|
132
|
+
return async (target, signal) => {
|
|
133
|
+
for (const guard of guards) {
|
|
134
|
+
try {
|
|
135
|
+
if ((await guard(target, signal)) !== false)
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return false;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
124
145
|
export function namespacedCoveringEntries(entries) {
|
|
125
146
|
return (entries ?? []).filter(isNamespacedCoveringRuleName);
|
|
126
147
|
}
|
|
@@ -452,6 +473,8 @@ export function createApprovalPolicy(opts) {
|
|
|
452
473
|
}
|
|
453
474
|
export function combinePolicies(...policies) {
|
|
454
475
|
const nameSets = policies.flatMap(toolPolicyNameSets);
|
|
476
|
+
const pathDenyRules = toolPolicyPathDenyRules(policies);
|
|
477
|
+
const writeTargetGuard = toolPolicyWriteTargetGuard(policies);
|
|
455
478
|
const childProjections = policies.map((p) => p.projection);
|
|
456
479
|
const projectedComponents = [];
|
|
457
480
|
let combinedRemainder = false;
|
|
@@ -472,6 +495,8 @@ export function combinePolicies(...policies) {
|
|
|
472
495
|
return {
|
|
473
496
|
...(combinedProjection !== undefined ? { projection: combinedProjection } : {}),
|
|
474
497
|
...(nameSets.length > 0 ? { nameSets } : {}),
|
|
498
|
+
...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
|
|
499
|
+
...(writeTargetGuard !== undefined ? { writeTargetGuard } : {}),
|
|
475
500
|
async check(req, signal) {
|
|
476
501
|
let asked;
|
|
477
502
|
let current = req;
|
|
@@ -482,6 +507,8 @@ export function combinePolicies(...policies) {
|
|
|
482
507
|
let probeMandateSeen = false;
|
|
483
508
|
let mandateSeen = false;
|
|
484
509
|
let fallbackSeen;
|
|
510
|
+
let authoredBySeen = false;
|
|
511
|
+
const contributors = [];
|
|
485
512
|
for (const p of policies) {
|
|
486
513
|
const d = refuseOutOfContractDecision(await p.check(current, signal));
|
|
487
514
|
if (d.action === "deny") {
|
|
@@ -516,6 +543,10 @@ export function combinePolicies(...policies) {
|
|
|
516
543
|
mandateSeen = true;
|
|
517
544
|
if (d.action === "ask" && d.denialLimitFallback !== undefined && fallbackSeen === undefined)
|
|
518
545
|
fallbackSeen = d.denialLimitFallback;
|
|
546
|
+
if (d.action === "ask" && d.authoredBy === "deployment")
|
|
547
|
+
authoredBySeen = true;
|
|
548
|
+
if (d.action === "ask")
|
|
549
|
+
contributors.push(d);
|
|
519
550
|
}
|
|
520
551
|
if (asked) {
|
|
521
552
|
const merged = rewrite?.updatedInput;
|
|
@@ -523,7 +554,14 @@ export function combinePolicies(...policies) {
|
|
|
523
554
|
const withMark = probeMandateSeen && withRuleAsk.probeMandated !== true ? { ...withRuleAsk, probeMandated: true } : withRuleAsk;
|
|
524
555
|
const withMandate = mandateSeen && withMark.mandated !== true ? { ...withMark, mandated: true } : withMark;
|
|
525
556
|
const withFallback = fallbackSeen !== undefined && withMandate.denialLimitFallback === undefined ? { ...withMandate, denialLimitFallback: fallbackSeen, requiresRealApproval: true } : withMandate;
|
|
526
|
-
|
|
557
|
+
const withAuthor = authoredBySeen && withFallback.authoredBy === undefined ? { ...withFallback, authoredBy: "deployment" } : withFallback;
|
|
558
|
+
const folded = new Set(withAuthor.foldedAskClasses ?? []);
|
|
559
|
+
for (const c of contributors)
|
|
560
|
+
if (c !== asked)
|
|
561
|
+
for (const k of decisionAskClassesOf(c))
|
|
562
|
+
folded.add(k);
|
|
563
|
+
const withClasses = folded.size > 0 ? { ...withAuthor, foldedAskClasses: [...folded] } : withAuthor;
|
|
564
|
+
return merged !== undefined ? { ...withClasses, updatedInput: merged } : withClasses;
|
|
527
565
|
}
|
|
528
566
|
const allowed = rewrite ?? ALLOW;
|
|
529
567
|
if (!anyRecord)
|
|
@@ -829,6 +867,7 @@ export function createUnverifiableDeletePolicy(opts) {
|
|
|
829
867
|
action: "ask",
|
|
830
868
|
decisionReason: "safety",
|
|
831
869
|
requiresRealApproval: true,
|
|
870
|
+
guard: "unverifiable_delete",
|
|
832
871
|
message: `Unverifiable recursive delete (fail-closed unless cleared): ${finding}. ` +
|
|
833
872
|
`Re-run the delete with the resolved literal path written into the command itself ` +
|
|
834
873
|
`(or assign the variable in the same command, e.g. \`DIR=/exact/path; rm -rf "$DIR"\`) so the target can be verified.`,
|
|
@@ -905,6 +944,7 @@ export function createTranscriptIntegrityPolicy(opts) {
|
|
|
905
944
|
action: "ask",
|
|
906
945
|
decisionReason: "safety",
|
|
907
946
|
requiresRealApproval: true,
|
|
947
|
+
guard: "transcript_write",
|
|
908
948
|
message: `Session-transcript write (fail-closed unless cleared): ${what}. Session transcripts (the .jsonl files ` +
|
|
909
949
|
`under the agent data dir's sessions/ directory) are harness-written session state, not agent working ` +
|
|
910
950
|
`files — modifying or deleting them tampers with the run's own audit trail. Reading them (ls/cat/grep ` +
|
|
@@ -956,6 +996,7 @@ export function isLiveApproverSeat(onAsk) {
|
|
|
956
996
|
return typeof onAsk === "function";
|
|
957
997
|
}
|
|
958
998
|
export const DURABLE_MANDATE_SOURCES = ["force_durable_gate", "durable_approval"];
|
|
999
|
+
Object.freeze(DURABLE_MANDATE_SOURCES);
|
|
959
1000
|
export function durableMandateSourceOf(args) {
|
|
960
1001
|
if (args.forceDurableGate)
|
|
961
1002
|
return "force_durable_gate";
|
|
@@ -1250,7 +1291,7 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
|
|
|
1250
1291
|
if (onAsk === undefined || onAsk === "deny") {
|
|
1251
1292
|
return {
|
|
1252
1293
|
action: "deny",
|
|
1253
|
-
message:
|
|
1294
|
+
message: headlessDenialMessage(req.toolName, req.message),
|
|
1254
1295
|
decisionReason: "mode",
|
|
1255
1296
|
settlement: settledByNobody("no_approver"),
|
|
1256
1297
|
};
|
|
@@ -28,6 +28,8 @@ export declare function pathConfinableWriteToolNames(): ReadonlySet<string>;
|
|
|
28
28
|
export declare function skillScopeWriteToolNames(): ReadonlySet<string>;
|
|
29
29
|
/** A-8 — every definition that names a path target at all (read or write): the file-tool glob lane. */
|
|
30
30
|
export declare function pathTargetingToolNames(): ReadonlySet<string>;
|
|
31
|
+
/** The tools whose declared path target is a READ (a file read, a search root): the file read station's membership. */
|
|
32
|
+
export declare function readTargetToolNames(): ReadonlySet<string>;
|
|
31
33
|
/** A-10 — the micro-compaction whitelist (`compactable: true`). */
|
|
32
34
|
export declare function compactableToolNames(): ReadonlySet<string>;
|
|
33
35
|
/** A-5 — the aggregate-budget exemptions (`budgetExempt: true`). */
|
|
@@ -31,6 +31,9 @@ export function skillScopeWriteToolNames() {
|
|
|
31
31
|
export function pathTargetingToolNames() {
|
|
32
32
|
return names((e) => e.face.pathTarget !== undefined);
|
|
33
33
|
}
|
|
34
|
+
export function readTargetToolNames() {
|
|
35
|
+
return names((e) => e.face.pathTarget?.access === "read");
|
|
36
|
+
}
|
|
34
37
|
export function compactableToolNames() {
|
|
35
38
|
return names((e) => e.face.compactable === true);
|
|
36
39
|
}
|
package/dist/core/tool-spec.d.ts
CHANGED
|
@@ -1113,7 +1113,7 @@ export interface ToolExecuteContext {
|
|
|
1113
1113
|
/**
|
|
1114
1114
|
* RB-201 FO-3 (form-one audit, CC 220 `Ipd`/`ein` parity) — the auto-mode classifier decider ARMED
|
|
1115
1115
|
* for THIS task (auto-mode intent ∧ `RunnerDeps.autoMode` present ∧ `RuntimeCaps.autoMode !== false`
|
|
1116
|
-
* — see {@link TaskSpec.
|
|
1116
|
+
* — see {@link TaskSpec.permissionMode}; the same
|
|
1117
1117
|
* instance `runToolGate`'s per-call ask review already consults, carrying its own live breaker
|
|
1118
1118
|
* state — not a fresh one built from raw config). Runner-filled, trusted, undefined when auto-mode
|
|
1119
1119
|
* is not armed or the tool runs outside a Runner task.
|
|
@@ -1286,13 +1286,12 @@ export interface ToolExecuteContext {
|
|
|
1286
1286
|
*/
|
|
1287
1287
|
requestStopAfterTurn?: () => void;
|
|
1288
1288
|
/**
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
* is
|
|
1295
|
-
* no-op for tools that don't call it. The Runner fills it; not a tool arg. Absent outside a Runner.
|
|
1289
|
+
* Enter plan mode mid-run: a steer of the leg's ONE permission-mode seat into `plan` — the same mode the spec's
|
|
1290
|
+
* `permissionMode` and `TaskStream.setPermissionMode` set. For the rest of THIS run the mode gate asks before every
|
|
1291
|
+
* covered write and every call to a tool the engine does not define that is not read-only (the plan sentence; refused
|
|
1292
|
+
* where nobody can answer), while read tools and `ExitPlanMode` run. The word it left is recorded: an approve-resume
|
|
1293
|
+
* returns to it; a rejected plan steers the resumed leg back into `plan`. The first-party `EnterPlanMode`
|
|
1294
|
+
* tool is the only caller. Idempotent. The Runner fills it; not a tool arg. Absent outside a Runner.
|
|
1296
1295
|
*/
|
|
1297
|
-
enterPlanMode?: () => void
|
|
1296
|
+
enterPlanMode?: () => Promise<void>;
|
|
1298
1297
|
}
|
package/dist/core/trace.d.ts
CHANGED
|
@@ -253,11 +253,12 @@ export type TraceEvent = {
|
|
|
253
253
|
taskId: string;
|
|
254
254
|
/** Which spec field the skipped entry came from. Absent = `additionalDirectories` (the
|
|
255
255
|
* original face, kept implicit for existing readers); `"additionalReadDirectories"` = the
|
|
256
|
-
* read-only whitelist arm (ruled 2026-08-05). */
|
|
257
|
-
field?: "additionalReadDirectories";
|
|
256
|
+
* read-only whitelist arm (ruled 2026-08-05); `"persistedReadRule"` = a person's persisted `Read(//dir/**)` allow row. */
|
|
257
|
+
field?: "additionalReadDirectories" | "persistedReadRule";
|
|
258
258
|
/** The spec entry as written (raw, not canonical — canonicalization is what failed). */
|
|
259
259
|
entry: string;
|
|
260
|
-
/**
|
|
260
|
+
/** Why the entry was skipped, `code: message` form: the canonicalization failure, or (a persisted row only)
|
|
261
|
+
* `admits_artifact_store` — the in-flight grant's store rule. */
|
|
261
262
|
reason: string;
|
|
262
263
|
ts: number;
|
|
263
264
|
} | {
|
|
@@ -442,9 +443,9 @@ export type TraceEvent = {
|
|
|
442
443
|
ts: number;
|
|
443
444
|
} | {
|
|
444
445
|
/**
|
|
445
|
-
* design/179 — the persisted
|
|
446
|
-
*
|
|
447
|
-
*
|
|
446
|
+
* design/179 — the person's persisted rule store could not be read (at prepare, or for one call), so
|
|
447
|
+
* that read's rows took no part: the decision it served was made with ZERO persisted rows. The operator's
|
|
448
|
+
* per-read record; the person hears it once per run (`permission.rule_store_unavailable`).
|
|
448
449
|
*/
|
|
449
450
|
kind: "permission.rule_store_unreadable";
|
|
450
451
|
version: 1;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -13,7 +13,8 @@ import type { AgentTool, ThinkingLevel, ToolInputValidationContext, ToolInputVer
|
|
|
13
13
|
import type { Model, StreamFn } from "../internal/llm.js";
|
|
14
14
|
export type { ModelRef, ModelRole, RoleSpec, ModelRoles, Brain } from "./model-seat.js";
|
|
15
15
|
export { MODEL_ROLES } from "./model-seat.js";
|
|
16
|
-
export type { StaleToolResultOffloadOptions, ImageInput, TaskSpec, ShellGateDoctrine
|
|
16
|
+
export type { StaleToolResultOffloadOptions, ImageInput, TaskSpec, ShellGateDoctrine } from "./task-spec.js";
|
|
17
|
+
export type { PermissionMode } from "./ask-origin.js";
|
|
17
18
|
export type { ToolEffect, ToolContentOrigin, ReversibilityVerdict, ReadRootGrantCandidate, ToolSpec, ToolReturn, NestedUsage, NestedUsageAccum, ToolExecuteContext } from "./tool-spec.js";
|
|
18
19
|
export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook, FileEditedNotice, FileEditedHook, HandsBandOptions } from "./hands-band.js";
|
|
19
20
|
export type { AgentDefinition, SkillManifest, SkillSpec } from "./agent-definition.js";
|
|
@@ -64,8 +64,8 @@ export type ParkLaneReason = "no_checkpoint_store" | "no_durable_approval_opt_in
|
|
|
64
64
|
* The closed set of auto-mode ARMING outcomes a leg's manifest reports
|
|
65
65
|
* ({@link WiringManifest.autoMode}). Minted from the arming predicate's own three-arm read, as the
|
|
66
66
|
* FIRST failing arm in predicate order:
|
|
67
|
-
* - `no_intent` —
|
|
68
|
-
* recorded on the resumed checkpoint
|
|
67
|
+
* - `no_intent` — the declared permission mode is not `auto` (no `permissionMode: "auto"` seat, none
|
|
68
|
+
* inherited on the chain / recorded on the resumed checkpoint; a non-auto run).
|
|
69
69
|
* - `no_face` — intent, but no `RunnerDeps.autoMode` deployment face (the trust gate).
|
|
70
70
|
* - `denied` — intent + face, but the organization's deny bit (`RuntimeCaps.autoMode:false`)
|
|
71
71
|
* as the resolver STATED it.
|
|
@@ -306,7 +306,8 @@ export interface WiringManifest {
|
|
|
306
306
|
* classifier decider was minted for this leg), and `reason` names the first arm that failed
|
|
307
307
|
* otherwise — see {@link AUTO_MODE_ARM_REASONS}. A serving layer that used to infer the mode from
|
|
308
308
|
* spec shape reads it here instead. The classifier's availability is a per-CALL fact (an unavailable
|
|
309
|
-
* round is a
|
|
309
|
+
* round goes on to the approval seat or is refused with a sentence that says so, and the next round is
|
|
310
|
+
* classified afresh), so there is no session-level
|
|
310
311
|
* breaker face here and nothing about availability enters {@link configFingerprint}.
|
|
311
312
|
*/
|
|
312
313
|
autoMode?: AutoModeArmFact;
|
|
@@ -369,6 +370,17 @@ export interface WiringManifest {
|
|
|
369
370
|
mounted: boolean;
|
|
370
371
|
reason?: HandsUnmountedReason;
|
|
371
372
|
};
|
|
373
|
+
/**
|
|
374
|
+
* EFFECTIVE half only, ALWAYS present on an engine-minted effective manifest: the leg's PERMISSION MODE as the engine
|
|
375
|
+
* resolved it — `requested` (the caller's word; absent = declared nothing), `effective` (the mode the leg runs under:
|
|
376
|
+
* the request clamped under a parent's ceiling, then folded to `default` where the deployment denies it or the
|
|
377
|
+
* classifier face is absent), `foldedBy` (present iff the two differ: `operator_deny` / `face_absent` /
|
|
378
|
+
* `inherited_ceiling`). The same three members answer every steer of the mode (`TaskStream.setPermissionMode`) and
|
|
379
|
+
* ride the fold notice's detail. A shell renders its mode indicator from `effective`, never from what it sent.
|
|
380
|
+
* ABSENT = an older mint, never a state. Leg-local (excluded from {@link configFingerprint}: a child clamped under its
|
|
381
|
+
* parent legitimately differs from it).
|
|
382
|
+
*/
|
|
383
|
+
permissionMode?: import("./mode-policy.js").PermissionModeReceipt;
|
|
372
384
|
/**
|
|
373
385
|
* EFFECTIVE half only, PRESENT IFF this leg compiled a write-protection judge — a deployment that
|
|
374
386
|
* replaced the table with `[]` opted the layer out entirely, and reports nothing here rather than a
|
|
@@ -407,8 +419,8 @@ export interface WiringManifest {
|
|
|
407
419
|
* of one assembly agree.
|
|
408
420
|
*
|
|
409
421
|
* @contract read_deny.builtin_activation_disclosed — WHETHER the built-in sensitive-path read deny
|
|
410
|
-
* table judges a leg is DISCLOSED, never inferred: the
|
|
411
|
-
*
|
|
422
|
+
* table judges a leg is DISCLOSED, never inferred: the tier selection is the deployment's (absent = the default
|
|
423
|
+
* tiers, an explicit `[]` = none), the leg that compiles the judge relays the tiers its own resolution materially
|
|
412
424
|
* activated, and the manifest carries that section iff at least one built-in row is active (absence
|
|
413
425
|
* = no built-in row judges; an empty array is never emitted — "off" and "on to nothing" are one
|
|
414
426
|
* fact). The disclosure changes no verdict.
|
|
@@ -524,6 +536,9 @@ export interface WiringFacts {
|
|
|
524
536
|
mounted: boolean;
|
|
525
537
|
reason?: HandsUnmountedReason;
|
|
526
538
|
};
|
|
539
|
+
/** Effective half only — the leg's permission-mode receipt ({@link WiringManifest.permissionMode}); the engine always passes it.
|
|
540
|
+
* Screened: `effective` a mode word, `requested` a mode word or absent, `foldedBy` a fold reason or absent. */
|
|
541
|
+
permissionMode?: import("./mode-policy.js").PermissionModeReceipt;
|
|
527
542
|
/** Effective half only — the compiled write-protection judge's OWN target-view word
|
|
528
543
|
* ({@link WiringManifest.writeProtection}); absent ⇒ this leg compiled no judge (an emptied table). */
|
|
529
544
|
writeProtectionTargetView?: WriteProtectionTargetView;
|
|
@@ -596,6 +611,17 @@ export declare function resolveSubagentTranscriptTier(agentStoreWired: boolean,
|
|
|
596
611
|
* else the headless degrade `auto_deny`.
|
|
597
612
|
*/
|
|
598
613
|
export declare function deriveAskEffective(form: AskSeamForm, parkEffective: boolean | "unresolved"): AskEffective;
|
|
614
|
+
/**
|
|
615
|
+
* Does an ask this leg hands on carrying `requiresRealApproval` reach a PERSON — never an automatic yes? The leg's
|
|
616
|
+
* APPROVAL SEAT, read off the ONE ask derivation above, never re-derived: `human_reachable` (a live approver) and
|
|
617
|
+
* `park_only` (the durable park; where the park cannot take the ask, a blanket seat behind it refuses the bit) are a
|
|
618
|
+
* person; `auto_allow`, `auto_deny` and `unresolved` are nobody (the last the fail-closed reading). The auto-mode
|
|
619
|
+
* classifier stations read it to decide where a classification that could not rule goes: to that person (the ask
|
|
620
|
+
* carrying the bit), or refused.
|
|
621
|
+
*/
|
|
622
|
+
export declare function askReachesPerson(ask: {
|
|
623
|
+
readonly effective?: AskEffective;
|
|
624
|
+
}): boolean;
|
|
599
625
|
export declare function deriveWiringManifest(facts: WiringFacts): WiringManifest;
|
|
600
626
|
/** The one ask-seat resolution (`spec` wins over `deps`), shared with the effective half so the
|
|
601
627
|
* form/provenance read cannot fork. Junk values (a JS caller passing something outside the OnAsk
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isPermissionMode } from "./ask-origin.js";
|
|
2
|
+
import { PERMISSION_MODE_FOLD_REASONS } from "./mode-policy.js";
|
|
1
3
|
import { createHash } from "node:crypto";
|
|
2
4
|
import { canonicalize } from "./canonical-json.js";
|
|
3
5
|
import { resolveCheckpointStore } from "./checkpoint-store.js";
|
|
@@ -57,6 +59,23 @@ export function deriveAskEffective(form, parkEffective) {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
}
|
|
62
|
+
export function askReachesPerson(ask) {
|
|
63
|
+
const effective = ask.effective ?? "unresolved";
|
|
64
|
+
switch (effective) {
|
|
65
|
+
case "human_reachable":
|
|
66
|
+
case "park_only":
|
|
67
|
+
return true;
|
|
68
|
+
case "auto_allow":
|
|
69
|
+
case "auto_deny":
|
|
70
|
+
case "unresolved":
|
|
71
|
+
return false;
|
|
72
|
+
default: {
|
|
73
|
+
const _exhaustive = effective;
|
|
74
|
+
void _exhaustive;
|
|
75
|
+
throw new Error(`unreachable ask effective ${String(effective)}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
60
79
|
function deriveParkLane(facts) {
|
|
61
80
|
if (!facts.parkCapable) {
|
|
62
81
|
return { capable: false, effective: false, reasons: ["no_checkpoint_store"] };
|
|
@@ -87,6 +106,13 @@ function readAutoModeFact(fact) {
|
|
|
87
106
|
}
|
|
88
107
|
return { armed, reason, ...(deniedSource !== undefined ? { deniedSource } : {}) };
|
|
89
108
|
}
|
|
109
|
+
function readPermissionModeFact(fact) {
|
|
110
|
+
const { requested, effective, foldedBy } = typeof fact === "object" && fact !== null ? fact : { requested: undefined, effective: undefined, foldedBy: undefined };
|
|
111
|
+
if (!isPermissionMode(effective) || (requested !== undefined && !isPermissionMode(requested)) || (foldedBy !== undefined && !PERMISSION_MODE_FOLD_REASONS.includes(foldedBy))) {
|
|
112
|
+
throw new Error(`the permission-mode wiring fact is inconsistent (${JSON.stringify(fact)}) — effective must be a permission mode, requested one or absent, foldedBy one of ${PERMISSION_MODE_FOLD_REASONS.join("|")} or absent`);
|
|
113
|
+
}
|
|
114
|
+
return { ...(requested !== undefined ? { requested } : {}), effective, ...(foldedBy !== undefined ? { foldedBy } : {}) };
|
|
115
|
+
}
|
|
90
116
|
function readHandsFact(fact) {
|
|
91
117
|
const { mounted, reason } = typeof fact === "object" && fact !== null ? fact : { mounted: undefined, reason: undefined };
|
|
92
118
|
if (typeof mounted !== "boolean" || (mounted ? reason !== undefined : reason !== "no_execution_env")) {
|
|
@@ -157,6 +183,7 @@ export function deriveWiringManifest(facts) {
|
|
|
157
183
|
...(facts.half === "effective" && facts.hooks !== undefined && facts.hooks.length > 0 ? { hooks: facts.hooks.map((h) => ({ ...h })) } : {}),
|
|
158
184
|
...(facts.half === "effective" && facts.lsp !== undefined ? { lsp: { ...facts.lsp } } : {}),
|
|
159
185
|
...(facts.half === "effective" && facts.hands !== undefined ? { hands: readHandsFact(facts.hands) } : {}),
|
|
186
|
+
...(facts.half === "effective" && facts.permissionMode !== undefined ? { permissionMode: readPermissionModeFact(facts.permissionMode) } : {}),
|
|
160
187
|
...(facts.half === "effective" && facts.writeProtectionTargetView !== undefined ? { writeProtection: { targetView: facts.writeProtectionTargetView } } : {}),
|
|
161
188
|
...(facts.half === "effective" && facts.readDenyBuiltinTiersActive !== undefined && facts.readDenyBuiltinTiersActive.length > 0
|
|
162
189
|
? { readDeny: { builtinTiers: [...facts.readDenyBuiltinTiersActive] } }
|
|
@@ -169,7 +196,7 @@ export function deriveWiringManifest(facts) {
|
|
|
169
196
|
: {}),
|
|
170
197
|
};
|
|
171
198
|
if (facts.half === "effective") {
|
|
172
|
-
const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, teamRole: _teamRole, peerPresence: _peerPresence, fileHistory: _fileHistory, ...assembly } = manifest;
|
|
199
|
+
const { leg: _leg, mcp: _mcp, tools: _tools, lsp: _lsp, hooks: _hooks, teamRole: _teamRole, peerPresence: _peerPresence, fileHistory: _fileHistory, permissionMode: _permissionMode, ...assembly } = manifest;
|
|
173
200
|
const { provenance: _askSeat, ...askForHash } = assembly.ask;
|
|
174
201
|
const { provenance: _questionSeat, ...questionForHash } = assembly.question;
|
|
175
202
|
manifest.configFingerprint = createHash("sha256")
|
|
@@ -20,7 +20,7 @@ export interface WriteProtectedRow {
|
|
|
20
20
|
* String shorthand: a bare name ≡ `{ name, kind: "segment" }` (the WIDER single-segment kind —
|
|
21
21
|
* over-matching is the fail-safe direction for a tighten); a name containing `/` ≡
|
|
22
22
|
* `{ name, kind: "segment-run" }`. Spell `kind: "basename"` explicitly when last-segment-only
|
|
23
|
-
* matching is the intent.
|
|
23
|
+
* matching is the intent. A name opening with the home word (`~/…`) refuses: the table places no row at a home.
|
|
24
24
|
*/
|
|
25
25
|
export type WriteProtectedEntry = string | WriteProtectedRow;
|
|
26
26
|
/** A table hit: which row matched (the row's canonical name + kind). */
|
|
@@ -108,6 +108,13 @@ export interface WriteProtectionJudgeContext {
|
|
|
108
108
|
readonly base?: string;
|
|
109
109
|
/** Bounds the filesystem probes of the target view. */
|
|
110
110
|
readonly signal?: AbortSignal;
|
|
111
|
+
/** The path is a CANONICAL key the caller already resolved at the seat its program writes it (the shell read station's
|
|
112
|
+
* established write target: where the path lands, or — for a removal or a rename — the entry itself, its canonical
|
|
113
|
+
* parent joined with the last name, a link there not followed). It is judged where a key is judged — the readings and
|
|
114
|
+
* the canonical data-root exemption — never as a spelling (the lexical passthrough reads a spelling, and a key reached
|
|
115
|
+
* through a linked data root does not spell itself inside it) and never followed: no link chain is walked from it, as
|
|
116
|
+
* the program walks none from where it writes. Without an environment the key is judged as a spelling (the absence law). */
|
|
117
|
+
readonly canonical?: true;
|
|
111
118
|
}
|
|
112
119
|
/**
|
|
113
120
|
* WHICH reading of a write's TARGET a compiled judge has — its own word, and the vocabulary the wiring
|
|
@@ -258,3 +265,6 @@ export interface WriteProtectionMount {
|
|
|
258
265
|
* `deny > ask > allow` fold, which is what makes the missing seat cost nothing.
|
|
259
266
|
*/
|
|
260
267
|
export declare function createWriteProtectionCheck(entries?: readonly WriteProtectedEntry[], opts?: WriteProtectionCompileOptions): WriteProtectionMount | undefined;
|
|
268
|
+
/** {@link createWriteProtectionCheck} over a judge already compiled — the engine's leg compiles once and hands the same
|
|
269
|
+
* judge to the call check and to the shell station (engine-internal; not re-exported from the package root). */
|
|
270
|
+
export declare function writeProtectionMountOver(matcher: WriteProtectionMatcher, rootPath: string | undefined): WriteProtectionMount;
|
|
@@ -3,6 +3,7 @@ import { protectivePathTargetOf } from "./tool-registry.js";
|
|
|
3
3
|
import { pathTargetBaseOf } from "./effective-path-target.js";
|
|
4
4
|
import { canonicalDataRootKeys, dataRootSpellings, relativeUnderRoot } from "./sensitive-path-policy.js";
|
|
5
5
|
import { symlinkChainHops } from "./path-identity.js";
|
|
6
|
+
import { readDenyPatternRun } from "../tools/fs/read-deny.js";
|
|
6
7
|
import { thrownText } from "./thrown-value.js";
|
|
7
8
|
const freezeTable = (rows) => Object.freeze(rows.map((r) => Object.freeze(r)));
|
|
8
9
|
export const WRITE_PROTECTED_DEFAULT_TABLE = freezeTable([
|
|
@@ -57,6 +58,7 @@ export const WRITE_PROTECTED_DEFAULT_TABLE = freezeTable([
|
|
|
57
58
|
{ name: ".gnupg", kind: "segment" },
|
|
58
59
|
{ name: ".sema.json", kind: "basename" },
|
|
59
60
|
{ name: ".sema", kind: "segment" },
|
|
61
|
+
{ name: "credentials.env", kind: "basename" },
|
|
60
62
|
]);
|
|
61
63
|
export function foldWriteProtectSegment(s) {
|
|
62
64
|
const cased = s.toLowerCase().replace(/ı/g, "i").replace(/ſ/g, "s");
|
|
@@ -91,6 +93,11 @@ function normalizeWriteProtectedEntry(entry) {
|
|
|
91
93
|
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(entry)} has kind ${describeEntryValue(shape.kind)} — known kinds: ${KINDS.join(", ")}.`);
|
|
92
94
|
}
|
|
93
95
|
const kind = shape.kind;
|
|
96
|
+
const { homeAnchored, run } = readDenyPatternRun(name);
|
|
97
|
+
if (homeAnchored) {
|
|
98
|
+
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(name)} opens with the home word "~", which this table does not place — its names are matched wherever their segments appear. ` +
|
|
99
|
+
(run.length > 0 ? `Spell the name without it (${describeEntryValue(run)}), which covers the home's file and every same-named one.` : "The home directory itself is not a row; spell the name of what it holds."));
|
|
100
|
+
}
|
|
94
101
|
if (name.includes("*") || name.includes("?")) {
|
|
95
102
|
throw new Error(`writeProtectedPaths: entry ${describeEntryValue(name)} contains a wildcard metacharacter — this table speaks LITERAL names only (a silently-literal "*" would guard less than it reads); pattern semantics live in createSensitivePathPolicy.`);
|
|
96
103
|
}
|
|
@@ -254,7 +261,8 @@ export function compileWriteProtection(entries, opts) {
|
|
|
254
261
|
return judgeSpelling(path, cwd);
|
|
255
262
|
},
|
|
256
263
|
async judge(path, ctx = {}) {
|
|
257
|
-
const
|
|
264
|
+
const givenKey = ctx.canonical === true && env !== undefined;
|
|
265
|
+
const bySpelling = givenKey ? null : judgeSpelling(path, ctx.cwd);
|
|
258
266
|
if (bySpelling !== null)
|
|
259
267
|
return { kind: "hit", hit: bySpelling, via: "spelling" };
|
|
260
268
|
if (env === undefined)
|
|
@@ -268,16 +276,18 @@ export function compileWriteProtection(entries, opts) {
|
|
|
268
276
|
noted = true;
|
|
269
277
|
opts?.onViewAbsent?.({ cause, unresolvedSymlink });
|
|
270
278
|
};
|
|
271
|
-
let canonKey;
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
279
|
+
let canonKey = givenKey ? path : undefined;
|
|
280
|
+
if (!givenKey) {
|
|
281
|
+
try {
|
|
282
|
+
const canon = await canonicalizeTarget(env, path, ctx.signal, family, base);
|
|
283
|
+
if (canon.ok)
|
|
284
|
+
canonKey = canon.key;
|
|
285
|
+
else
|
|
286
|
+
noteAbsence(canon.message, canon.unresolvedSymlink === true);
|
|
287
|
+
}
|
|
288
|
+
catch (err) {
|
|
289
|
+
noteAbsence(thrownText(err), false);
|
|
290
|
+
}
|
|
281
291
|
}
|
|
282
292
|
try {
|
|
283
293
|
if (canonKey !== undefined) {
|
|
@@ -295,7 +305,7 @@ export function compileWriteProtection(entries, opts) {
|
|
|
295
305
|
return { kind: "hit", hit: byTarget, via: "target" };
|
|
296
306
|
}
|
|
297
307
|
}
|
|
298
|
-
for (const hop of await symlinkChainHops(env, path, ctx.signal, family, base, (cause) => noteAbsence(cause, false))) {
|
|
308
|
+
for (const hop of givenKey ? [] : await symlinkChainHops(env, path, ctx.signal, family, base, (cause) => noteAbsence(cause, false))) {
|
|
299
309
|
const byHop = judgeSpelled(hop);
|
|
300
310
|
if (byHop === null)
|
|
301
311
|
continue;
|
|
@@ -346,8 +356,9 @@ export function writeProtectionUnresolved(verdict) {
|
|
|
346
356
|
}
|
|
347
357
|
export function createWriteProtectionCheck(entries, opts) {
|
|
348
358
|
const matcher = compileWriteProtection(entries, opts);
|
|
349
|
-
|
|
350
|
-
|
|
359
|
+
return matcher === undefined ? undefined : writeProtectionMountOver(matcher, opts?.rootPath);
|
|
360
|
+
}
|
|
361
|
+
export function writeProtectionMountOver(matcher, rootPath) {
|
|
351
362
|
const check = (toolName, args, face, cwd, signal) => {
|
|
352
363
|
const call = { toolName, args, ...(face !== undefined ? { face } : {}) };
|
|
353
364
|
const protective = protectivePathTargetOf(call);
|
|
@@ -356,7 +367,7 @@ export function createWriteProtectionCheck(entries, opts) {
|
|
|
356
367
|
const path = writeTargetPath(call, protective);
|
|
357
368
|
if (typeof path !== "string" || path.length === 0)
|
|
358
369
|
return null;
|
|
359
|
-
return matcher.judge(path, { ...(cwd !== undefined ? { cwd } : {}), ...(signal !== undefined ? { signal } : {}), ...(() => { const b = pathTargetBaseOf(protective, { root:
|
|
370
|
+
return matcher.judge(path, { ...(cwd !== undefined ? { cwd } : {}), ...(signal !== undefined ? { signal } : {}), ...(() => { const b = pathTargetBaseOf(protective, { root: rootPath, cwd }); return b !== undefined ? { base: b } : {}; })() });
|
|
360
371
|
};
|
|
361
372
|
return { check, targetView: matcher.targetView };
|
|
362
373
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { scrubSecretEnv } from "../../core/secret-env.js";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
const DEFAULT_GRACE_MS = 3000;
|
|
@@ -144,6 +145,7 @@ function enumerateDescendantsSync(rootPid) {
|
|
|
144
145
|
let out;
|
|
145
146
|
try {
|
|
146
147
|
const result = spawnSync("ps", ["-A", "-o", "pid=", "-o", "ppid=", "-o", "etime="], {
|
|
148
|
+
env: scrubSecretEnv(process.env),
|
|
147
149
|
timeout: PS_ENUMERATION_TIMEOUT_MS,
|
|
148
150
|
encoding: "utf8",
|
|
149
151
|
windowsHide: true,
|
|
@@ -209,6 +211,7 @@ function parseEtimeSeconds(etime) {
|
|
|
209
211
|
function runTaskkill(args) {
|
|
210
212
|
try {
|
|
211
213
|
const child = spawn("taskkill", args, {
|
|
214
|
+
env: scrubSecretEnv(process.env),
|
|
212
215
|
stdio: "ignore",
|
|
213
216
|
detached: true,
|
|
214
217
|
windowsHide: true,
|
|
@@ -268,7 +271,7 @@ function tryMsysGroupKill(bashPath, pid) {
|
|
|
268
271
|
`[ -n "$g" ] && [ "$g" != "0" ] && kill -9 -"$g" 2>/dev/null; ` +
|
|
269
272
|
`done; exit 0`;
|
|
270
273
|
try {
|
|
271
|
-
const child = spawn(bashPath, ["-c", script], { stdio: "ignore", detached: true, windowsHide: true });
|
|
274
|
+
const child = spawn(bashPath, ["-c", script], { env: scrubSecretEnv(process.env), stdio: "ignore", detached: true, windowsHide: true });
|
|
272
275
|
child.on("error", () => { });
|
|
273
276
|
return true;
|
|
274
277
|
}
|
|
@@ -187,6 +187,9 @@ export declare class NodeExecutionEnv implements ExecutionEnv, BackgroundShellCa
|
|
|
187
187
|
setFileMode(path: string, mode: number, abortSignal?: AbortSignal): Promise<Result<void, FileError>>;
|
|
188
188
|
listDir(path: string, abortSignal?: AbortSignal): Promise<Result<FileInfo[], FileError>>;
|
|
189
189
|
canonicalPath(path: string): Promise<Result<string, FileError>>;
|
|
190
|
+
/** {@link FileSystem.fileIdentity}: `stat` (links followed) read as BIGINT — an inode past 2^53 (an APFS volume root)
|
|
191
|
+
* is not exact as a number, and two inodes that collapse to one number would name two directories as one. */
|
|
192
|
+
fileIdentity(path: string): Promise<Result<string, FileError>>;
|
|
190
193
|
readLink(path: string): Promise<Result<string, FileError>>;
|
|
191
194
|
/**
|
|
192
195
|
* The LEAF symlink chain ({@link FileSystem.canonicalChain}): `readlink` on the whole spelling, then on each
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { closeSync, constants, createReadStream, mkdtempSync, openSync, readSync, statSync, truncateSync, unlinkSync } from "node:fs";
|
|
4
|
-
import { access, appendFile, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, rename, rm, unlink, writeFile, } from "node:fs/promises";
|
|
4
|
+
import { access, appendFile, lstat, mkdir, mkdtemp, open, readdir, readFile, readlink, rename, rm, stat, unlink, writeFile, } from "node:fs/promises";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
7
7
|
import { createInterface } from "node:readline";
|
|
8
8
|
import { ExecutionError, err, FileError, ok, toError, } from "../harness/types.js";
|
|
9
9
|
import { killProcessTree, shutdownDebug } from "./kill-tree.js";
|
|
10
|
-
import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, scrubSecretEnv } from "../../core/secret-env.js";
|
|
10
|
+
import { __resetSecretEnvScrubNoticeAnnouncement, announceSecretEnvScrub, inheritedHostEnv, scrubSecretEnv } from "../../core/secret-env.js";
|
|
11
11
|
import { physicalPathOfAsync } from "../../core/physical-path.js";
|
|
12
12
|
import { isUncSpelling } from "../../tools/fs/safety.js";
|
|
13
13
|
import { RollingTailBuffer, markTruncated, newStreamCursorState, sliceStreamIncrement, } from "../../core/exec-output-tail.js";
|
|
@@ -119,6 +119,7 @@ async function runCommand(command, args, timeoutMs) {
|
|
|
119
119
|
child = spawn(command, args, {
|
|
120
120
|
stdio: ["ignore", "pipe", "ignore"],
|
|
121
121
|
windowsHide: true,
|
|
122
|
+
env: scrubSecretEnv(process.env),
|
|
122
123
|
});
|
|
123
124
|
}
|
|
124
125
|
catch {
|
|
@@ -203,44 +204,32 @@ export function __resetSecretScrubAnnouncement() {
|
|
|
203
204
|
__resetSecretEnvScrubNoticeAnnouncement();
|
|
204
205
|
}
|
|
205
206
|
function getShellEnv(inheritEnv, baseEnv, extraEnv, onScrub, onNotice) {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
else {
|
|
217
|
-
const scrubbed = [];
|
|
218
|
-
inherited = scrubSecretEnv(process.env, scrubbed, process.platform);
|
|
219
|
-
const findings = scrubbed.filter((f) => baseEnv?.[f.key] === undefined && extraEnv?.[f.key] === undefined);
|
|
220
|
-
if (findings.length > 0) {
|
|
221
|
-
if (typeof onScrub === "function") {
|
|
222
|
-
try {
|
|
223
|
-
const r = onScrub(findings);
|
|
224
|
-
if (typeof r?.then === "function") {
|
|
225
|
-
r.then(undefined, () => {
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
catch {
|
|
207
|
+
const scrubbed = [];
|
|
208
|
+
const inherited = inheritedHostEnv(inheritEnv, scrubbed);
|
|
209
|
+
const findings = scrubbed.filter((f) => baseEnv?.[f.key] === undefined && extraEnv?.[f.key] === undefined);
|
|
210
|
+
if (findings.length > 0) {
|
|
211
|
+
if (typeof onScrub === "function") {
|
|
212
|
+
try {
|
|
213
|
+
const r = onScrub(findings);
|
|
214
|
+
if (typeof r?.then === "function") {
|
|
215
|
+
r.then(undefined, () => {
|
|
216
|
+
});
|
|
230
217
|
}
|
|
231
218
|
}
|
|
232
|
-
|
|
233
|
-
secretScrubSeatDefectAnnounced = true;
|
|
234
|
-
try {
|
|
235
|
-
console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
|
|
236
|
-
}
|
|
237
|
-
catch {
|
|
238
|
-
}
|
|
219
|
+
catch {
|
|
239
220
|
}
|
|
240
|
-
|
|
241
|
-
|
|
221
|
+
}
|
|
222
|
+
else if (onScrub !== undefined && !secretScrubSeatDefectAnnounced) {
|
|
223
|
+
secretScrubSeatDefectAnnounced = true;
|
|
224
|
+
try {
|
|
225
|
+
console.warn(`onSecretEnvScrub seat holds ${onScrub === null ? "null" : typeof onScrub} — not a function; scrub findings fall back to the console summary (once per process)`);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
242
228
|
}
|
|
243
229
|
}
|
|
230
|
+
if (!(typeof onScrub === "function" && onNotice === undefined)) {
|
|
231
|
+
announceSecretEnvScrub(onNotice, findings);
|
|
232
|
+
}
|
|
244
233
|
}
|
|
245
234
|
return {
|
|
246
235
|
...inherited,
|
|
@@ -1021,6 +1010,16 @@ export class NodeExecutionEnv {
|
|
|
1021
1010
|
return err(toFileError(error, resolved));
|
|
1022
1011
|
}
|
|
1023
1012
|
}
|
|
1013
|
+
async fileIdentity(path) {
|
|
1014
|
+
const resolved = resolvePath(this.cwd, path);
|
|
1015
|
+
try {
|
|
1016
|
+
const s = await stat(resolved, { bigint: true });
|
|
1017
|
+
return ok(`${s.dev}:${s.ino}`);
|
|
1018
|
+
}
|
|
1019
|
+
catch (error) {
|
|
1020
|
+
return err(toFileError(error, resolved));
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1024
1023
|
async readLink(path) {
|
|
1025
1024
|
const resolved = resolvePath(this.cwd, path);
|
|
1026
1025
|
try {
|