@sema-agent/core 7.42.1 → 7.44.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 +173 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +3 -1
- package/dist/agents/subagent.js +5 -3
- package/dist/agents/teacher.js +1 -1
- package/dist/agents/verify.js +2 -0
- package/dist/brain/errors.d.ts +23 -2
- package/dist/brain/errors.js +26 -2
- package/dist/brain/stream-engine.d.ts +2 -1
- package/dist/brain/stream-engine.js +10 -9
- package/dist/core/allow-entry.d.ts +50 -31
- package/dist/core/allow-entry.js +100 -37
- package/dist/core/ask-origin.d.ts +16 -5
- package/dist/core/ask-origin.js +11 -3
- package/dist/core/ask-unresolvable-notice.d.ts +3 -2
- package/dist/core/ask-unresolvable-notice.js +1 -1
- package/dist/core/auto-mode.d.ts +5 -4
- package/dist/core/auto-mode.js +3 -1
- package/dist/core/checkpoint-store.d.ts +44 -20
- package/dist/core/checkpoint-store.js +10 -16
- package/dist/core/child-clamp-seats.d.ts +15 -1
- package/dist/core/child-clamp-seats.js +6 -2
- package/dist/core/classifier-unavailable-arm.d.ts +42 -23
- package/dist/core/classifier-unavailable-arm.js +14 -3
- package/dist/core/decision-receipt.js +2 -0
- package/dist/core/engine-notice.d.ts +16 -14
- package/dist/core/engine-notice.js +4 -3
- package/dist/core/gate-lanes.js +37 -29
- package/dist/core/governance-codes.d.ts +4 -3
- package/dist/core/governance-codes.js +1 -0
- package/dist/core/hooks.d.ts +6 -1
- package/dist/core/hooks.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +2 -2
- package/dist/core/memory-engine/engine.js +7 -14
- package/dist/core/memory-engine/file-backend.d.ts +2 -2
- package/dist/core/memory-engine/file-backend.js +5 -7
- package/dist/core/memory-engine/layout.d.ts +37 -4
- package/dist/core/memory-engine/layout.js +34 -5
- package/dist/core/mode-policy.d.ts +5 -4
- package/dist/core/mode-policy.js +4 -0
- package/dist/core/permission-rule-consent.d.ts +3 -2
- package/dist/core/permission-rule-consent.js +25 -20
- package/dist/core/permission-rule-model.d.ts +47 -7
- package/dist/core/permission-rule-model.js +80 -6
- package/dist/core/permission-rules.d.ts +8 -2
- package/dist/core/permission-rules.js +26 -9
- package/dist/core/runner/classifier-ladder.d.ts +10 -8
- package/dist/core/runner/classifier-ladder.js +3 -1
- package/dist/core/runner/contracts.d.ts +1 -16
- package/dist/core/runner/denial-limit-arms.d.ts +25 -8
- package/dist/core/runner/denial-limit-arms.js +24 -10
- package/dist/core/runner/gate-exit.js +2 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +2 -2
- package/dist/core/runner/permission-rule-lanes.js +8 -2
- package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
- package/dist/core/runner/prepare-ask-lane.js +2 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +3 -2
- package/dist/core/runner/prepare-config-doors.d.ts +3 -0
- package/dist/core/runner/prepare-config-doors.js +6 -5
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
- package/dist/core/runner/prepare-inherited-gate.js +5 -3
- package/dist/core/runner/prepare-park-ask.d.ts +4 -4
- package/dist/core/runner/prepare-park-ask.js +2 -4
- package/dist/core/runner/prepare-policy-chain.d.ts +7 -1
- package/dist/core/runner/prepare-policy-chain.js +59 -38
- package/dist/core/runner/prepare-project-context.js +6 -2
- package/dist/core/runner/prepare-task.js +6 -6
- package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -5
- package/dist/core/runner/prepare-wiring-manifest.js +8 -9
- package/dist/core/runner/resume-checkpoint-screen.js +1 -1
- package/dist/core/runner/runtask.js +1 -5
- package/dist/core/runner/steer-admission.d.ts +10 -11
- package/dist/core/runner/steer-admission.js +6 -8
- package/dist/core/runner/stream-steer-verb.d.ts +5 -4
- package/dist/core/runner/stream-steer-verb.js +6 -8
- package/dist/core/runner/synthetic-tools.d.ts +3 -2
- package/dist/core/runner/synthetic-tools.js +1 -1
- package/dist/core/session-policy-store.d.ts +3 -3
- package/dist/core/session-policy-store.js +18 -4
- package/dist/core/skill-grant-binding.d.ts +44 -0
- package/dist/core/skill-grant-binding.js +22 -0
- package/dist/core/skill-grant.d.ts +59 -28
- package/dist/core/skill-grant.js +96 -23
- package/dist/core/steer-replay.d.ts +46 -0
- package/dist/core/steer-replay.js +22 -0
- package/dist/core/task-event.d.ts +3 -2
- package/dist/core/task-spec.d.ts +14 -1
- package/dist/core/task-stream.d.ts +12 -6
- package/dist/core/tighten-task-spec.d.ts +2 -0
- package/dist/core/tighten-task-spec.js +3 -0
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-name-aliases.d.ts +35 -5
- package/dist/core/tool-name-aliases.js +29 -4
- package/dist/core/tool-policy.d.ts +10 -0
- package/dist/core/tool-policy.js +9 -2
- package/dist/core/tool-spec.d.ts +7 -0
- package/dist/core/types.d.ts +2 -2
- package/dist/core/types.js +1 -1
- package/dist/core/wiring-manifest.d.ts +56 -14
- package/dist/core/wiring-manifest.js +29 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +5 -4
- package/dist/orchestration/run-workflow-tool.d.ts +4 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +3 -1
- package/dist/orchestration/workflow-primitives.js +6 -6
- package/dist/orchestration/workflow-types.d.ts +5 -0
- package/dist/orchestration/workflow.d.ts +3 -2
- package/dist/orchestration/workflow.js +14 -6
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +21 -5
- package/wire-changes/core@7.43.0.json +985 -0
- package/wire-changes/core@7.44.0.json +469 -0
package/dist/core/gate-lanes.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, refuseOutOfContractDecision } from "./tool-policy.js";
|
|
2
2
|
import { arbitrateByMode, classifyAsk, fileCommandOptionsSentence, persistedRuleMandateOf, ruleClearanceOf, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
|
|
3
|
-
import { classifierParseFailureDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
|
|
4
|
-
import {
|
|
3
|
+
import { classifierParseFailureDenyMessage, denialLimitFallbackMessage, rungDidNotRule, unarmedWindow } from "./auto-mode.js";
|
|
4
|
+
import { classifierNoRulingArm, settleNoRulingAsk } from "./classifier-unavailable-arm.js";
|
|
5
5
|
import { SETTLEMENT_NOBODY_JUDGED } from "./gate-outcome.js";
|
|
6
6
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
7
7
|
import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
|
|
@@ -228,6 +228,12 @@ export async function runGateLanes(pass) {
|
|
|
228
228
|
pass.decision = shadows.length > 0 ? { ...standing, allowRuleShadowed: shadows } : standing;
|
|
229
229
|
}
|
|
230
230
|
const originFacts = askOriginFacts(orgAnswer.org, ruleCallUnreadableOf(laneAnswer));
|
|
231
|
+
let noRulingMinted = false;
|
|
232
|
+
const fallbackAskOf = (standingAsk, fallback, latestBlockedAction) => {
|
|
233
|
+
const { classifierNoRuling: _why, ...standing } = standingAsk;
|
|
234
|
+
const fallbackAsk = { ...standing, message: denialLimitFallbackMessage(fallback, latestBlockedAction), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback };
|
|
235
|
+
return stampAskOrigin(fallbackAsk, originFacts, classifyStanding(fallbackAsk, originFacts));
|
|
236
|
+
};
|
|
231
237
|
if (pass.decision.action === "ask") {
|
|
232
238
|
const stamped = { ...pass.decision, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}) };
|
|
233
239
|
pass.decision = stampAskOrigin(stamped, originFacts, classifyStanding(stamped, originFacts));
|
|
@@ -271,36 +277,31 @@ export async function runGateLanes(pass) {
|
|
|
271
277
|
...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}),
|
|
272
278
|
};
|
|
273
279
|
}
|
|
274
|
-
else if (verdict
|
|
275
|
-
const
|
|
276
|
-
|
|
280
|
+
else if (verdict !== undefined) {
|
|
281
|
+
const noRuling = rungDidNotRule(verdict);
|
|
282
|
+
const arm = noRuling !== undefined ? classifierNoRulingArm({ ask: pass.decision, req: pass.req, cause: noRuling, attribution: verdict.classifier, seat: input.autoMode.unavailable, aborted: classifySignal?.aborted === true }) : undefined;
|
|
283
|
+
if (arm?.kind === "asked") {
|
|
284
|
+
noRulingMinted = true;
|
|
277
285
|
pass.decision = stampAskOrigin(arm.ask, originFacts, classifyStanding(arm.ask, originFacts));
|
|
278
286
|
pass.classifierRound = undefined;
|
|
279
287
|
}
|
|
280
|
-
else
|
|
288
|
+
else if (arm?.kind === "denied")
|
|
281
289
|
pass.decision = arm.deny;
|
|
282
|
-
}
|
|
283
|
-
else if (verdict !== undefined) {
|
|
284
|
-
const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
|
|
285
|
-
const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
286
|
-
const tracked = input.autoMode.denialTracking?.recordBlock();
|
|
287
|
-
if (tracked?.limitReached === true) {
|
|
288
|
-
const fallbackAsk = {
|
|
289
|
-
...pass.decision,
|
|
290
|
-
message: denialLimitFallbackMessage(tracked.fallback, reason || category || toolName),
|
|
291
|
-
decisionReason: "classifier",
|
|
292
|
-
requiresRealApproval: true,
|
|
293
|
-
denialLimitFallback: tracked.fallback,
|
|
294
|
-
};
|
|
295
|
-
pass.decision = stampAskOrigin(fallbackAsk, originFacts, classifyStanding(fallbackAsk, originFacts));
|
|
296
|
-
}
|
|
297
290
|
else {
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
291
|
+
const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
|
|
292
|
+
const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
|
|
293
|
+
const tracked = input.autoMode.denialTracking?.recordBlock();
|
|
294
|
+
if (tracked?.limitReached === true) {
|
|
295
|
+
pass.decision = fallbackAskOf(pass.decision, tracked.fallback, reason || category || toolName);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
const denied = {
|
|
299
|
+
action: "deny",
|
|
300
|
+
message: verdict.kind === "block" ? `auto-mode classifier blocked this call${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : reason,
|
|
301
|
+
decisionReason: "classifier",
|
|
302
|
+
};
|
|
303
|
+
pass.decision = verdict.kind === "parse_error" ? attestClassifierDenyCause(denied, "parse_error", pass.req) : denied;
|
|
304
|
+
}
|
|
304
305
|
}
|
|
305
306
|
}
|
|
306
307
|
if (pass.decision.action === "deny") {
|
|
@@ -389,6 +390,7 @@ export async function runGateLanes(pass) {
|
|
|
389
390
|
const carryOf = (d) => ({
|
|
390
391
|
...(callSignal !== undefined ? { signal: callSignal } : {}),
|
|
391
392
|
...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
|
|
393
|
+
...(d.action === "ask" && d.classifierNoRuling !== undefined ? { classifierNoRuling: d.classifierNoRuling } : {}),
|
|
392
394
|
...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
|
|
393
395
|
...(d.action === "ask" && d.askClass !== undefined && d.classes !== undefined ? { askClass: d.askClass, classes: d.classes } : {}),
|
|
394
396
|
...(d.action === "ask" && d.mandated === true ? { mandated: true, ...(d.mandate !== undefined ? { mandate: d.mandate } : {}) } : {}),
|
|
@@ -444,8 +446,14 @@ export async function runGateLanes(pass) {
|
|
|
444
446
|
}
|
|
445
447
|
}
|
|
446
448
|
if (pass.decision.action === "ask") {
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
+
let askBeforeResolve = pass.decision;
|
|
450
|
+
const resolveOnce = (d) => (callSignal !== undefined ? resolveAsk(d, pass.req, callSignal) : resolveAsk(d, pass.req));
|
|
451
|
+
let resolved = await resolveOnce(askBeforeResolve);
|
|
452
|
+
const refallback = settleNoRulingAsk({ ask: askBeforeResolve, mintedHere: noRulingMinted, resolved, tracker: input.autoMode?.denialTracking });
|
|
453
|
+
if (refallback !== undefined && askBeforeResolve.action === "ask") {
|
|
454
|
+
askBeforeResolve = fallbackAskOf(askBeforeResolve, refallback, toolName);
|
|
455
|
+
resolved = await resolveOnce(askBeforeResolve);
|
|
456
|
+
}
|
|
449
457
|
ledger.fromResolved(resolved, askBeforeResolve, toolName);
|
|
450
458
|
if (resolved.action === "deny")
|
|
451
459
|
pass.deniedBy = "ask_resolution";
|
|
@@ -259,9 +259,10 @@ export declare function noticeAudienceOf(code: string): NoticeAudience;
|
|
|
259
259
|
* {@link NOTICE_AUDIENCE} like every other row, and NEVER minted by this package (the catalog gate reds a mint site for
|
|
260
260
|
* one in src/). One code, one minter: a code two processes mint would be two facts under one spelling.
|
|
261
261
|
*
|
|
262
|
-
* Member-level exception, stated where a consumer reads the minter: `task.user_steer_undrained`
|
|
263
|
-
*
|
|
264
|
-
* (see `
|
|
262
|
+
* Member-level exception, stated where a consumer reads the minter: `task.user_steer_undrained` and
|
|
263
|
+
* `task.user_followup_undrained` are engine rows, and their `detail.cause: "crashed"` arm is minted by the server — the
|
|
264
|
+
* engine's process is gone when that loss is counted (see `USER_INPUT_UNDRAINED_CAUSES`); the server picks the code by the
|
|
265
|
+
* lane the input was queued on, as this engine does. Each code keeps one row here and one spelling for both arms.
|
|
265
266
|
*/
|
|
266
267
|
export type NoticeMinter = "engine" | "server";
|
|
267
268
|
declare const NOTICE_MINTER_TABLE: {
|
|
@@ -84,6 +84,7 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
84
84
|
"memory.capture_optout_unavailable",
|
|
85
85
|
"memory.capture_optout_sweep_failed",
|
|
86
86
|
"memory.target_outside_store",
|
|
87
|
+
"memory.scope_home_not_directory",
|
|
87
88
|
]);
|
|
88
89
|
export function governanceRetryClass(code) {
|
|
89
90
|
if (Object.prototype.hasOwnProperty.call(GOVERNANCE_CODES, code)) {
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-
|
|
|
7
7
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
8
8
|
import { type AskClass } from "./ask-class.js";
|
|
9
9
|
import { type PermissionMode } from "./ask-origin.js";
|
|
10
|
-
import type
|
|
10
|
+
import { type AutoModeDenialTracker } from "./auto-mode.js";
|
|
11
11
|
export { formatHookFeedback } from "./reminder-mint.js";
|
|
12
12
|
import type { WiringLegKind } from "./wiring-manifest.js";
|
|
13
13
|
import { type SafeNotifier } from "./safe-notify.js";
|
|
@@ -1464,6 +1464,8 @@ export interface ToolGateInput extends PermissionSeatInputs, SkillGateSeats, All
|
|
|
1464
1464
|
* `autoMode` presence alone as the arming fact.
|
|
1465
1465
|
*/
|
|
1466
1466
|
permissionMode?: () => import("./ask-origin.js").PermissionMode;
|
|
1467
|
+
/** The leg's approval-seat form (`WiringManifest.ask.form`): the deny exit's way-out clause names a declaration, never a missing approver. */
|
|
1468
|
+
askSeatForm?: import("./wiring-manifest.js").AskSeamForm;
|
|
1467
1469
|
/** WHO put the tool's egress / irreversibility marks on the seat ({@link import("./ask-origin.js").ToolMarkSource});
|
|
1468
1470
|
* absent or empty with a mark present reads as the deployment's own (fail-closed: an operator's mandate). */
|
|
1469
1471
|
toolMarkSources?: ReadonlySet<import("./ask-origin.js").ToolMarkSource>;
|
|
@@ -1764,6 +1766,8 @@ export interface AskCarry {
|
|
|
1764
1766
|
* the `0` a park row shows is written. Persisted on the row so a durable card shows the counts the
|
|
1765
1767
|
* synchronous card shows. Absent then means: not a fallback ask. */
|
|
1766
1768
|
readonly denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
|
|
1769
|
+
/** The surviving ask's no-ruling member ({@link import("./tool-policy.js").AskRequest.classifierNoRuling}), for the row's twin. */
|
|
1770
|
+
readonly classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
|
|
1767
1771
|
/** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
|
|
1768
1772
|
* on the row so a durable card is rendered by the same word the synchronous card is. */
|
|
1769
1773
|
readonly origin?: import("./ask-origin.js").AskOrigin;
|
|
@@ -1788,6 +1792,7 @@ export interface AskCarry {
|
|
|
1788
1792
|
* the same screen the summary applies to the word). */
|
|
1789
1793
|
export declare function askCarryRowMembers(carry: AskCarry | undefined): {
|
|
1790
1794
|
denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
|
|
1795
|
+
classifierNoRuling?: import("./auto-mode.js").AutoModeFallbackCause;
|
|
1791
1796
|
origin?: import("./ask-origin.js").AskOrigin;
|
|
1792
1797
|
askClass?: import("./ask-origin.js").AskClass;
|
|
1793
1798
|
classes?: readonly import("./ask-origin.js").AskClass[];
|
package/dist/core/hooks.js
CHANGED
|
@@ -8,6 +8,7 @@ export { normalizeOrgGateVerdict, normalizePersistedRuleHit } from "./rule-lane-
|
|
|
8
8
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
9
9
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
10
10
|
import { isAskOrigin, screenAskClassMembers } from "./ask-origin.js";
|
|
11
|
+
import { isAutoModeFallbackCause } from "./auto-mode.js";
|
|
11
12
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
12
13
|
export { formatHookFeedback } from "./reminder-mint.js";
|
|
13
14
|
import { createSafeNotifier } from "./safe-notify.js";
|
|
@@ -242,9 +243,11 @@ export function askCarryRowMembers(carry) {
|
|
|
242
243
|
if (carry === undefined)
|
|
243
244
|
return {};
|
|
244
245
|
const origin = carry.origin;
|
|
246
|
+
const why = carry.classifierNoRuling;
|
|
245
247
|
const classed = screenAskClassMembers(carry.askClass, carry.classes);
|
|
246
248
|
return {
|
|
247
249
|
...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
|
|
250
|
+
...(isAutoModeFallbackCause(why) ? { classifierNoRuling: why } : {}),
|
|
248
251
|
...(isAskOrigin(origin) ? { origin } : {}),
|
|
249
252
|
...(classed !== undefined ? classed : {}),
|
|
250
253
|
};
|
|
@@ -1789,7 +1789,7 @@ export declare class MemoryEngine {
|
|
|
1789
1789
|
*/
|
|
1790
1790
|
private gateDerivedIndex;
|
|
1791
1791
|
/** The file backend's verdict at an act the engine itself performs on a store path ({@link
|
|
1792
|
-
*
|
|
1792
|
+
* landsOutsideStore}): throws `memory.target_outside_store` when `path` would land outside `root`;
|
|
1793
1793
|
* each caller's existing failure lane reports it. `root` is the mount's canonical root where a
|
|
1794
1794
|
* handle exists, so a root re-pointed after the mount is judged against the one it mounted. */
|
|
1795
1795
|
private refuseOutsideStore;
|
|
@@ -1798,7 +1798,7 @@ export declare class MemoryEngine {
|
|
|
1798
1798
|
* is never a supported layout (the harvest scan refuses symlinked entry files outright), and this
|
|
1799
1799
|
* site already reports write failures by throwing, so ELOOP joins the failures it already had. A
|
|
1800
1800
|
* target whose directory resolves outside `root` (a directory on the path is a link) is refused
|
|
1801
|
-
* the same way, before anything is read or written ({@link
|
|
1801
|
+
* the same way, before anything is read or written ({@link landsOutsideStore}). */
|
|
1802
1802
|
private writeIfChanged;
|
|
1803
1803
|
/** Persist the canonical projection back to the session file (id minting / frontmatter completion).
|
|
1804
1804
|
* C-F6 (S2-0): called ONLY after the backend transaction committed — never ahead of the journal
|
|
@@ -18,7 +18,7 @@ import { landedUndecided, layerDigest, layerObservationLookup, readOnlyLayerCand
|
|
|
18
18
|
import { markOriginClearanceTombstoned, openOriginClearance, readOriginClearances, settleOriginClearance } from "./origin-clearance.js";
|
|
19
19
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, canonicalJsonStringify, captureErasureInput, erasureRequestInvalid, erasureSelectHash, scanEntryFiles, } from "./file-backend.js";
|
|
20
20
|
import { assembleMemoryExportBundle, computeMemoryBundleHash, memoryBundleInvalid, } from "./export-bundle.js";
|
|
21
|
-
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, indexLineIsStale, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, landingPathOf,
|
|
21
|
+
import { QUARANTINE_DIR, SCAN_FUSE_THRESHOLD, quarantineAndTombstone, restoreContainedBaseline, indexLineIsStale, readIndexRevs, writeIndexRevs, bumpScanFuse, scanFuseKey, canonicalize, landingPathOf, landsOutsideStore, storeActRefusal, storeRootMovedRefusal, adoptRootScopeLayout, clearScanFuse, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, drainMemoryAnnouncements, enqueueMemoryAnnouncement, ensureDirExists, markSessionPolluted, readSessionPollution, fileSessionCaptureRecordStore, recordRetrievedAccount, writeFileNoFollow, withPermissionRemedy, permissionRemedyOf, readRetrievedAccount, registerScope, registeredScopes, resolveMemoryEngineRoot, scopeDirFor, appendChallengeEvents, appendLineageAudit, rebuildStrictControlPlaneLedger, isStrictControlPlaneLedgerCorrupt, CHALLENGE_LEDGER_MAX_EVENTS, adjudicateLineagePending, challengedEntryIds, clearLineageForEntries, discardLineagePending, lineageAccountOfEntry, lineageContributionsOfSession, lineageLatchedIds, promoteLineagePending, readChallengeEvents, readChallengedHistory, readLineageRecord, recordChallengedHistory, recordLineageCredential, reconcileLineage, readProjectionDebts, resolveChallengeEvent, settleProjectionDebts, stageLineagePending, stageProjectionDebts, } from "./layout.js";
|
|
22
22
|
import { createSessionIncarnationSeat, recalledForAnyIdentity, recalledForIdentity, rememberForIdentity } from "./session-incarnation.js";
|
|
23
23
|
import { scanMemoryFileName, scanMemoryWrite, scanRemediation } from "./scan.js";
|
|
24
24
|
import { writableRootOf } from "./types.js";
|
|
@@ -1416,11 +1416,8 @@ export class MemoryEngine {
|
|
|
1416
1416
|
ensureDirExists(this.memoryDir);
|
|
1417
1417
|
const physicalRoot = landingPathOf(this.memoryDir);
|
|
1418
1418
|
this.mountedRoot ??= physicalRoot;
|
|
1419
|
-
if (physicalRoot !== this.mountedRoot)
|
|
1420
|
-
|
|
1421
|
-
e.code = TARGET_OUTSIDE_STORE;
|
|
1422
|
-
throw e;
|
|
1423
|
-
}
|
|
1419
|
+
if (physicalRoot !== this.mountedRoot)
|
|
1420
|
+
throw storeRootMovedRefusal("engine", this.memoryDir);
|
|
1424
1421
|
ensureDirExists(this.controlDir);
|
|
1425
1422
|
await this.adoptLayoutBeforeMount(writeScope);
|
|
1426
1423
|
const restricted = opts?.adoptionRestricted === true;
|
|
@@ -2216,7 +2213,7 @@ export class MemoryEngine {
|
|
|
2216
2213
|
if (content === undefined)
|
|
2217
2214
|
continue;
|
|
2218
2215
|
try {
|
|
2219
|
-
this.refuseOutsideStore(handle.memoryDir, m.path, "restoring");
|
|
2216
|
+
this.refuseOutsideStore(handle.memoryDir, m.path, "restoring a memory file");
|
|
2220
2217
|
mkdirSync(dirname(m.path), { recursive: true });
|
|
2221
2218
|
writeFileNoFollow(m.path, content);
|
|
2222
2219
|
report.restored.push(m.relPath);
|
|
@@ -4947,15 +4944,11 @@ export class MemoryEngine {
|
|
|
4947
4944
|
};
|
|
4948
4945
|
}
|
|
4949
4946
|
refuseOutsideStore(root, path, act) {
|
|
4950
|
-
|
|
4951
|
-
|
|
4952
|
-
return;
|
|
4953
|
-
const e = new Error(`${TARGET_OUTSIDE_STORE}: ${act} ${path} was refused — ${escape}; not performed`);
|
|
4954
|
-
e.code = TARGET_OUTSIDE_STORE;
|
|
4955
|
-
throw e;
|
|
4947
|
+
if (landsOutsideStore(root, path))
|
|
4948
|
+
throw storeActRefusal(act, path);
|
|
4956
4949
|
}
|
|
4957
4950
|
writeIfChanged(target, text, root) {
|
|
4958
|
-
this.refuseOutsideStore(root, target, "the projection write
|
|
4951
|
+
this.refuseOutsideStore(root, target, "the projection write");
|
|
4959
4952
|
const current = readNoFollowSafe(target);
|
|
4960
4953
|
if (current === text)
|
|
4961
4954
|
return;
|
|
@@ -682,9 +682,9 @@ export declare class FileMemoryEngineBackend implements MemoryBackend {
|
|
|
682
682
|
* let a transient EIO run the unlocked recovery (and its staged sweep) over a live transaction. */
|
|
683
683
|
private txnInFlight;
|
|
684
684
|
/**
|
|
685
|
-
* The act gate for store paths ({@link
|
|
685
|
+
* The act gate for store paths ({@link landsOutsideStore}) over `ops`' writes and deletes — shadow
|
|
686
686
|
* ops name control-plane files, not store paths. Refuses with `memory.target_outside_store` at the
|
|
687
|
-
* first op that would land outside the store, naming the op
|
|
687
|
+
* first op that would land outside the store, naming the op (its path on `detail.path`). Runs over a
|
|
688
688
|
* whole batch before its commit marker (no journal is written; a caller that staged files removes
|
|
689
689
|
* them) and per op at the act itself; past the marker a refusal keeps the journal, which replays
|
|
690
690
|
* once the path resolves inside the store again.
|
|
@@ -8,7 +8,7 @@ import { ambiguousOriginRepresentation, committedDistilledOf, committedOriginOf,
|
|
|
8
8
|
import { convertV1SidecarKeys } from "./delegation-settlement.js";
|
|
9
9
|
import { layerObservationLookup, observedDivergenceNotice } from "./layer-observations.js";
|
|
10
10
|
import { DEFAULT_MAX_ENTRY_DEPTH, MEMORY_INDEX_FILENAME, scanEntryFiles } from "./entry-scan.js";
|
|
11
|
-
import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, adoptRootScopeLayout, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, permissionRemedyOf, withPermissionRemedy, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, layoutNeedsAdoption, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, scopeRegistryShape, stageImportLatch, landingPathOf,
|
|
11
|
+
import { ControlPlaneCorruptError, CURSORS_FILE, QUARANTINE_DIR, CHALLENGES_FILE, IMPORT_RECEIPTS_DIR, LINEAGE_FILE, SESSION_POLLUTION_DIR, appendChallengeEvents, atomicWriteFileSync, quarantineAndTombstone, captureAndClearLineageForEntries, adoptRootScopeLayout, adoptCanonicalKeyedControlDir, deriveControlPlaneDir, ensureDirExists, enqueueMemoryAnnouncement, importLatchTxnId, importLineageCommitted, importSessionPollution, isContainedIn, permissionRemedyOf, withPermissionRemedy, lineageContributionsOfSession, listSessionPollution, readChallengeEvents, readLineageRecord, registerScope, registeredScopes, releaseImportLatch, layoutNeedsAdoption, resolveMemoryEngineRoot, scopeDirFor, scopeDirName, scopeRegistryShape, stageImportLatch, landingPathOf, landsOutsideStore, storeActRefusal, storeRootMovedRefusal, writeAllSync, } from "./layout.js";
|
|
12
12
|
import { screenInboundEntries } from "./data-plane.js";
|
|
13
13
|
import { scanMemoryFileName, scanMemoryWrite } from "./scan.js";
|
|
14
14
|
import { thrownText } from "../thrown-value.js";
|
|
@@ -1404,7 +1404,7 @@ export class FileMemoryEngineBackend {
|
|
|
1404
1404
|
}
|
|
1405
1405
|
}
|
|
1406
1406
|
this.assertTxnLockOwnership(fenceToken);
|
|
1407
|
-
const replayAt =
|
|
1407
|
+
const replayAt = "the journal replay (journal kept)";
|
|
1408
1408
|
this.refuseEscapingTargets(journal.ops, replayAt);
|
|
1409
1409
|
for (const op of journal.ops) {
|
|
1410
1410
|
this.refuseEscapingTargets([op], replayAt);
|
|
@@ -1470,10 +1470,8 @@ export class FileMemoryEngineBackend {
|
|
|
1470
1470
|
if (op.kind !== "write" && op.kind !== "delete")
|
|
1471
1471
|
continue;
|
|
1472
1472
|
for (const path of op.staged === undefined ? [op.target] : [op.target, op.staged]) {
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
continue;
|
|
1476
|
-
throw erasureCodedError(`${TARGET_OUTSIDE_STORE}: ${at} refused a ${op.kind} of ${op.target}${path === op.target ? "" : ` (its staged file ${path})`} — ${escape}; not performed`, TARGET_OUTSIDE_STORE);
|
|
1473
|
+
if (landsOutsideStore(this.physicalRoot, path))
|
|
1474
|
+
throw storeActRefusal(`${path === op.target ? "a" : "the staged file of a"} ${op.kind} at ${at}`, path);
|
|
1477
1475
|
}
|
|
1478
1476
|
}
|
|
1479
1477
|
}
|
|
@@ -1481,7 +1479,7 @@ export class FileMemoryEngineBackend {
|
|
|
1481
1479
|
const now = landingPathOf(this.directoryRoot);
|
|
1482
1480
|
if (now === this.physicalRoot)
|
|
1483
1481
|
return;
|
|
1484
|
-
throw
|
|
1482
|
+
throw storeRootMovedRefusal("backend", this.directoryRoot);
|
|
1485
1483
|
}
|
|
1486
1484
|
sweepStagedFiles() {
|
|
1487
1485
|
const walk = (d, depth) => {
|
|
@@ -259,8 +259,41 @@ export declare function adoptRootScopeLayout(rootPath: string, controlDir: strin
|
|
|
259
259
|
export declare const canonicalize: typeof physicalPathOfExisting;
|
|
260
260
|
/** True iff `child` (canonicalized) is `root` or inside it. */
|
|
261
261
|
export declare function isContainedIn(root: string, child: string): boolean;
|
|
262
|
-
/** The code an act on a store path is refused with when it would land outside the store ({@link
|
|
262
|
+
/** The code an act on a store path is refused with when it would land outside the store ({@link landsOutsideStore}). */
|
|
263
263
|
export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
|
|
264
|
+
/** The code a scope home that is not a real directory is refused with ({@link assertHomePhysicalForm}). */
|
|
265
|
+
export declare const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
|
|
266
|
+
/** What a scope home can be besides a real directory (an `lstat` verdict). */
|
|
267
|
+
export declare const SCOPE_HOME_FORMS: readonly ["symlink", "file", "other"];
|
|
268
|
+
export type ScopeHomeForm = (typeof SCOPE_HOME_FORMS)[number];
|
|
269
|
+
/** How a refusal's sentence names each form. */
|
|
270
|
+
declare const SCOPE_HOME_FORM_PHRASE: {
|
|
271
|
+
readonly symlink: "a symbolic link";
|
|
272
|
+
readonly file: "a regular file";
|
|
273
|
+
readonly other: "a special file";
|
|
274
|
+
};
|
|
275
|
+
export type HomeFormPhraseCoversEveryForm = AssertAllKeysHandled<Exclude<ScopeHomeForm, keyof typeof SCOPE_HOME_FORM_PHRASE>>;
|
|
276
|
+
/** What a store-form refusal carries beside its sentence. The ONE host path lives here and never in the
|
|
277
|
+
* sentence; `form` is present on {@link SCOPE_HOME_NOT_DIRECTORY} only. */
|
|
278
|
+
export type StoreFormRefusalDetail = {
|
|
279
|
+
readonly path: string;
|
|
280
|
+
readonly form?: ScopeHomeForm;
|
|
281
|
+
};
|
|
282
|
+
/** A refusal the store's physical form earns: a plain `Error` (never the control-plane-damage class —
|
|
283
|
+
* the engine's own account is intact, the layout it was handed is not the layout it needs) carrying a
|
|
284
|
+
* `code` and a `detail`. */
|
|
285
|
+
export type StoreFormRefusal = Error & {
|
|
286
|
+
code: typeof TARGET_OUTSIDE_STORE | typeof SCOPE_HOME_NOT_DIRECTORY;
|
|
287
|
+
detail: StoreFormRefusalDetail;
|
|
288
|
+
};
|
|
289
|
+
/** Seat 1 — an act the store's physical form refuses: `act` names what was refused (no path), `path` is the
|
|
290
|
+
* target whose directory resolves outside the store ({@link landsOutsideStore}). */
|
|
291
|
+
export declare function storeActRefusal(act: string, path: string): StoreFormRefusal;
|
|
292
|
+
/** Seat 2 — the store root now resolves to another directory than the one the `holder` first opened; `path` is
|
|
293
|
+
* the root's spelling. */
|
|
294
|
+
export declare function storeRootMovedRefusal(holder: "backend" | "engine", path: string): StoreFormRefusal;
|
|
295
|
+
/** Seat 3 — a scope home that is not a real directory. */
|
|
296
|
+
export declare function scopeHomeRefusal(form: ScopeHomeForm, path: string): StoreFormRefusal;
|
|
264
297
|
/** Where an act at `p` lands: the physical path of its normalized spelling (the spelling a child path
|
|
265
298
|
* joined under it carries), a MISSING tail rejoined under the deepest existing directory (what the act
|
|
266
299
|
* creates). Any other resolution fault (EACCES, ELOOP, ENOTDIR) is THROWN as itself: an act that
|
|
@@ -268,8 +301,8 @@ export declare const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
|
|
|
268
301
|
* (a link whose own mode refuses `realpath` is still followed by `unlink` and `rename`). */
|
|
269
302
|
export declare function landingPathOf(p: string): string;
|
|
270
303
|
/**
|
|
271
|
-
*
|
|
272
|
-
* `physicalRoot
|
|
304
|
+
* Whether a write, rename or delete at `target` would physically LAND outside the store, judged against the
|
|
305
|
+
* store's `physicalRoot` (`true` = outside; the refusal is {@link storeActRefusal}). The target's directory, resolved through
|
|
273
306
|
* every link on its existing prefix (a missing tail is what the act creates, under the deepest existing
|
|
274
307
|
* directory), must be the root or inside it. `physicalRoot` is the root's physical form as the store
|
|
275
308
|
* was opened and is not re-resolved here: a store mounted through a link is judged by what the link
|
|
@@ -280,7 +313,7 @@ export declare function landingPathOf(p: string): string;
|
|
|
280
313
|
* keeps the spelling while the act follows the link out. A directory that cannot be resolved throws its
|
|
281
314
|
* fault ({@link landingPathOf}).
|
|
282
315
|
*/
|
|
283
|
-
export declare function
|
|
316
|
+
export declare function landsOutsideStore(physicalRoot: string, target: string): boolean;
|
|
284
317
|
/** mkdir -p convenience used by the engine/backend (0o700 like the file stores). */
|
|
285
318
|
export declare function ensureDirExists(dir: string): void;
|
|
286
319
|
/** The one remedy every surfaced permission refusal carries — `undefined` unless `err` is a write
|
|
@@ -472,11 +472,18 @@ function assertHomePhysicalForm(home) {
|
|
|
472
472
|
return "absent";
|
|
473
473
|
if (st.isDirectory())
|
|
474
474
|
return "dir";
|
|
475
|
-
throw
|
|
475
|
+
throw scopeHomeRefusal(st.isSymbolicLink() ? "symlink" : st.isFile() ? "file" : "other", home);
|
|
476
476
|
}
|
|
477
477
|
function ensureScopeHome(home) {
|
|
478
|
-
if (assertHomePhysicalForm(home) === "absent")
|
|
479
|
-
|
|
478
|
+
if (assertHomePhysicalForm(home) === "absent") {
|
|
479
|
+
try {
|
|
480
|
+
mkdirSync(home, { recursive: true });
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
assertHomePhysicalForm(home);
|
|
484
|
+
throw err;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
480
487
|
assertHomePhysicalForm(home);
|
|
481
488
|
}
|
|
482
489
|
export function registerScope(memoryDir, controlDir, scope, opts) {
|
|
@@ -725,6 +732,28 @@ export function isContainedIn(root, child) {
|
|
|
725
732
|
return c === r || c.startsWith(`${r}${sep}`);
|
|
726
733
|
}
|
|
727
734
|
export const TARGET_OUTSIDE_STORE = "memory.target_outside_store";
|
|
735
|
+
export const SCOPE_HOME_NOT_DIRECTORY = "memory.scope_home_not_directory";
|
|
736
|
+
export const SCOPE_HOME_FORMS = Object.freeze(["symlink", "file", "other"]);
|
|
737
|
+
const SCOPE_HOME_FORM_PHRASE = {
|
|
738
|
+
symlink: "a symbolic link",
|
|
739
|
+
file: "a regular file",
|
|
740
|
+
other: "a special file",
|
|
741
|
+
};
|
|
742
|
+
function storeFormRefusal(code, sentence, detail) {
|
|
743
|
+
const e = new Error(`${code}: ${sentence}`);
|
|
744
|
+
e.code = code;
|
|
745
|
+
e.detail = detail;
|
|
746
|
+
return e;
|
|
747
|
+
}
|
|
748
|
+
export function storeActRefusal(act, path) {
|
|
749
|
+
return storeFormRefusal(TARGET_OUTSIDE_STORE, `${act} was refused — its directory resolves outside the store root; not performed`, { path });
|
|
750
|
+
}
|
|
751
|
+
export function storeRootMovedRefusal(holder, path) {
|
|
752
|
+
return storeFormRefusal(TARGET_OUTSIDE_STORE, `the ${holder === "backend" ? "store root" : "memory dir"} now resolves to a different directory than the one this ${holder} ${holder === "backend" ? "opened" : "first mounted"} — refused; open a new ${holder} on the directory you mean`, { path });
|
|
753
|
+
}
|
|
754
|
+
export function scopeHomeRefusal(form, path) {
|
|
755
|
+
return storeFormRefusal(SCOPE_HOME_NOT_DIRECTORY, `a scope home must be a real directory, but this one is ${SCOPE_HOME_FORM_PHRASE[form]} — refused (fail-closed); put the real directory back`, { path, form });
|
|
756
|
+
}
|
|
728
757
|
export function landingPathOf(p) {
|
|
729
758
|
const normalized = resolve(p);
|
|
730
759
|
try {
|
|
@@ -736,9 +765,9 @@ export function landingPathOf(p) {
|
|
|
736
765
|
return canonicalize(normalized);
|
|
737
766
|
}
|
|
738
767
|
}
|
|
739
|
-
export function
|
|
768
|
+
export function landsOutsideStore(physicalRoot, target) {
|
|
740
769
|
const dir = landingPathOf(dirname(target));
|
|
741
|
-
return dir
|
|
770
|
+
return dir !== physicalRoot && !dir.startsWith(`${physicalRoot}${sep}`);
|
|
742
771
|
}
|
|
743
772
|
export function ensureDirExists(dir) {
|
|
744
773
|
if (!existsSync(dir))
|
|
@@ -163,17 +163,18 @@ export declare function modeRequestAfterResume(seat: PermissionMode | undefined,
|
|
|
163
163
|
export declare function refusePermissionModeDoors(spec: Pick<TaskSpec, "permissionMode" | "shellGate">): PermissionMode | undefined;
|
|
164
164
|
/**
|
|
165
165
|
* The doors a RESUMED leg's prepare refuses at, read as one screen for the resume ladder to run BEFORE the claim: the mode
|
|
166
|
-
* doors on the re-supplied config ({@link refusePermissionModeDoors}), its output-style seat (`resolveOutputStyleFrame`)
|
|
167
|
-
*
|
|
166
|
+
* doors on the re-supplied config ({@link refusePermissionModeDoors}), its output-style seat (`resolveOutputStyleFrame`), its
|
|
167
|
+
* approval-reach declaration (`screenApprovalReach`), the skills it invokes by name (`screenUserInvokedSkills` — the part no
|
|
168
|
+
* roster changes; a name the leg's roster reads as nothing is prepare's) and the mode words the checkpoint row recorded (`recordedModeSeed`) — prepare's own functions, so the refusal carries the very
|
|
168
169
|
* sentence and code prepare would throw. It leaves by the resume's ONE refusal channel, `CheckpointError`
|
|
169
170
|
* `checkpoint.config_refused` (`message` = prepare's sentence, `detail.code` = prepare's code, `detail.field` = the input that
|
|
170
171
|
* carries it: `"taskConfig"` or `"inheritedGate"`), where prepare itself would end the run `failed` after the claim — a
|
|
171
172
|
* refusal there consumes the row and reopens it as a system retry that replays the recorded decision, which a CONFIG
|
|
172
173
|
* correction cannot reach. Before the claim the row stays `pending` and byte-identical, and the same token is redeemable by a
|
|
173
|
-
* corrected resume. A throw that is not one of prepare's
|
|
174
|
+
* corrected resume. A throw that is not one of prepare's five codes (a caller's accessor failing) is not a refusal and leaves
|
|
174
175
|
* as it was thrown.
|
|
175
176
|
*/
|
|
176
|
-
export declare function refuseResumeDoors(config: Pick<TaskSpec, "permissionMode" | "shellGate" | "outputStyle">, recordedGate: Parameters<typeof recordedModeSeed>[0]): void;
|
|
177
|
+
export declare function refuseResumeDoors(config: Pick<TaskSpec, "permissionMode" | "shellGate" | "outputStyle" | "approvalReach" | "skills" | "userInvokedSkills">, recordedGate: Parameters<typeof recordedModeSeed>[0]): void;
|
|
177
178
|
/** The three-member answer every mode question gets — the manifest member, the steer verb's return value and the
|
|
178
179
|
* fold notice's detail share it. `requested` is absent when the caller declared nothing (a root inherits `default`,
|
|
179
180
|
* a child inherits its parent). */
|
package/dist/core/mode-policy.js
CHANGED
|
@@ -9,6 +9,8 @@ import { createSafeNotifier } from "./safe-notify.js";
|
|
|
9
9
|
import { assertNoRetiredKeys, RETIRED_TASK_SPEC_KEYS } from "./retired-keys.js";
|
|
10
10
|
import { resolveOutputStyleFrame } from "./output-style.js";
|
|
11
11
|
import { CheckpointError, isCheckpointConfigRefusalCode, recordedModeSeed } from "./checkpoint-store.js";
|
|
12
|
+
import { screenApprovalReach } from "./wiring-manifest.js";
|
|
13
|
+
import { screenUserInvokedSkills } from "./skill-grant.js";
|
|
12
14
|
export const PERMISSION_MODE_FOLD_REASONS = Object.freeze(["operator_deny", "face_absent", "inherited_ceiling"]);
|
|
13
15
|
export const ACCEPT_EDITS_FILE_COMMANDS = Object.freeze(["mkdir", "touch", "rm", "rmdir", "mv", "cp", "sed"]);
|
|
14
16
|
const MODE_POLICY_TABLE = {
|
|
@@ -112,6 +114,8 @@ export function refuseResumeDoors(config, recordedGate) {
|
|
|
112
114
|
screenAs("taskConfig", () => {
|
|
113
115
|
refusePermissionModeDoors(config);
|
|
114
116
|
resolveOutputStyleFrame(config.outputStyle);
|
|
117
|
+
screenApprovalReach(config.approvalReach);
|
|
118
|
+
screenUserInvokedSkills(config);
|
|
115
119
|
});
|
|
116
120
|
screenAs("inheritedGate", () => void recordedModeSeed(recordedGate));
|
|
117
121
|
}
|
|
@@ -282,9 +282,10 @@ export declare function prepareCardApproval(opts: {
|
|
|
282
282
|
principal?: string;
|
|
283
283
|
/** design/182 §4.5 (additive): the structural owner — only the local-owner path needs it. */
|
|
284
284
|
owner?: RuleOwner;
|
|
285
|
-
/** The tool whose ask this card belongs to. Only the shell tool the rule lane speaks for
|
|
285
|
+
/** The tool whose ask this card belongs to. Only the shell tool the rule lane speaks for and the name tools (the skill-activation
|
|
286
|
+
* card) are accepted. */
|
|
286
287
|
toolName: string;
|
|
287
|
-
/** The command as adjudicated — the same bytes the ask carried. */
|
|
288
|
+
/** The command as adjudicated — the same bytes the ask carried; for a name tool (`Skill`), the name its call carried (`args.skill`). */
|
|
288
289
|
command: string;
|
|
289
290
|
/** The ask's call id and argument digest, recorded for reconciliation. */
|
|
290
291
|
toolCallId?: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, segmentCoverageOf, suggestRulesForCommand, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
|
|
2
|
+
import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, ruleToolGrammarOf, segmentCoverageOf, suggestRuleForName, suggestRulesForCommand, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
|
|
3
3
|
import { effectiveOrThrow } from "./permission-rule-provider.js";
|
|
4
4
|
import { errText, isValidConsentScope, landRuleAdd, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
5
5
|
export class InMemoryRuleApprovalRecordStore {
|
|
@@ -279,7 +279,8 @@ export async function prepareCardApproval(opts) {
|
|
|
279
279
|
e.code = "config.invalid_argument";
|
|
280
280
|
throw e;
|
|
281
281
|
}
|
|
282
|
-
|
|
282
|
+
const nameCard = ruleToolGrammarOf(opts.toolName) === "name";
|
|
283
|
+
if (opts.toolName !== CARD_RULE_TOOL && !nameCard)
|
|
283
284
|
return undefined;
|
|
284
285
|
const scope = (() => {
|
|
285
286
|
if (opts.scope !== undefined) {
|
|
@@ -303,22 +304,26 @@ export async function prepareCardApproval(opts) {
|
|
|
303
304
|
throw e;
|
|
304
305
|
})();
|
|
305
306
|
let coverage;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
307
|
+
if (!nameCard) {
|
|
308
|
+
try {
|
|
309
|
+
const view = await effectiveOrThrow(storeForOwner(opts.deps.provider, owner), opts.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined);
|
|
310
|
+
const table = view.rules;
|
|
311
|
+
coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: CARD_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId, ...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}) });
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
coverage = undefined;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const suggested = nameCard
|
|
318
|
+
? [suggestRuleForName(opts.toolName, opts.command)].filter((o) => o !== undefined)
|
|
319
|
+
: suggestRulesForCommand(opts.command, {
|
|
320
|
+
...(coverage !== undefined ? { coverage } : {}),
|
|
321
|
+
scope,
|
|
322
|
+
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
323
|
+
...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}),
|
|
324
|
+
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
325
|
+
...(opts.deps.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: opts.deps.deniesDirectoryRead } : {}),
|
|
326
|
+
});
|
|
322
327
|
const candidates = [];
|
|
323
328
|
const claimedBy = new Map();
|
|
324
329
|
const claim = (text) => {
|
|
@@ -349,7 +354,7 @@ export async function prepareCardApproval(opts) {
|
|
|
349
354
|
rev: 0,
|
|
350
355
|
candidates,
|
|
351
356
|
offers,
|
|
352
|
-
command: opts.command,
|
|
357
|
+
...(nameCard ? {} : { command: opts.command }),
|
|
353
358
|
createdAt: nowIso(opts.deps),
|
|
354
359
|
...(opts.toolCallId !== undefined ? { toolCallId: opts.toolCallId } : {}),
|
|
355
360
|
...(opts.boundInputHash !== undefined ? { boundInputHash: opts.boundInputHash } : {}),
|
|
@@ -510,7 +515,7 @@ async function confirmEditedCandidate(rec, edit, deps) {
|
|
|
510
515
|
return no("edit_rejected", { message: "the record's candidates carry no single common scope — an edited candidate inherits the card's scope, and a record without one is malformed" });
|
|
511
516
|
}
|
|
512
517
|
if (rec.command === undefined) {
|
|
513
|
-
return no("edit_rejected", { message: "the record does not carry
|
|
518
|
+
return no("edit_rejected", { message: "the record does not carry an adjudicated command (a name card, or one minted before card edits existed) — coverage cannot be verified, so the edit is refused" });
|
|
514
519
|
}
|
|
515
520
|
const checked = checkEditedRuleText(edit.text, rec.command);
|
|
516
521
|
if (!checked.ok) {
|