@sema-agent/core 5.28.0 → 5.30.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 +99 -3
- package/dist/agents/cascade.d.ts +1 -1
- package/dist/agents/observer.d.ts +3 -3
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/agents/send-message-tool.js +2 -0
- package/dist/agents/subagent.d.ts +2 -0
- package/dist/agents/subagent.js +6 -0
- package/dist/agents/teacher.d.ts +1 -1
- package/dist/agents/teacher.js +2 -0
- package/dist/agents/verify.d.ts +6 -6
- package/dist/agents/verify.js +2 -0
- package/dist/bench/metrics.d.ts +1 -1
- package/dist/brain/retry.d.ts +1 -1
- package/dist/core/arg-summary.d.ts +1 -1
- package/dist/core/ask-class.d.ts +2 -2
- package/dist/core/ask-question.d.ts +1 -1
- package/dist/core/ask-question.js +14 -1
- package/dist/core/auto-compaction.d.ts +5 -1
- package/dist/core/auto-compaction.js +10 -1
- package/dist/core/background-shell.d.ts +5 -5
- package/dist/core/checkpoint-store.d.ts +65 -19
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/file-snapshot-store.d.ts +1 -1
- package/dist/core/hooks.d.ts +15 -4
- package/dist/core/hooks.js +8 -2
- package/dist/core/human-input-projection.d.ts +2 -2
- package/dist/core/memory-admission.d.ts +2 -2
- package/dist/core/memory-engine/engine.d.ts +1 -1
- package/dist/core/memory-engine/file-backend.d.ts +2 -2
- package/dist/core/memory-engine/header-hints.d.ts +1 -1
- package/dist/core/memory-engine/layout.d.ts +2 -2
- package/dist/core/memory-recall.d.ts +1 -1
- package/dist/core/memory.d.ts +2 -2
- package/dist/core/oracle-isolation.d.ts +2 -2
- package/dist/core/permission-rule-consent.d.ts +1 -1
- package/dist/core/permission-rule-model.d.ts +1 -1
- package/dist/core/permission-rule-org.d.ts +1 -1
- package/dist/core/permission-rule-store.d.ts +1 -1
- package/dist/core/permission-rules.js +2 -2
- package/dist/core/runner/active-skill-scope.d.ts +1 -1
- package/dist/core/runner/memory-consolidation.d.ts +1 -1
- package/dist/core/runner/prepare-task.d.ts +29 -8
- package/dist/core/runner/prepare-task.js +118 -22
- package/dist/core/runner/runtask.d.ts +1 -1
- package/dist/core/runner/runtask.js +34 -8
- package/dist/core/runner/session-rule-policy.d.ts +4 -3
- package/dist/core/runner/teardown-bounded.d.ts +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +1 -1
- package/dist/core/runner/tool-output-projection.js +1 -1
- package/dist/core/scheduler.d.ts +4 -4
- package/dist/core/sensitive-path-policy.js +5 -16
- package/dist/core/session-policy-store.d.ts +3 -3
- package/dist/core/shared-memory/normalize.d.ts +1 -1
- package/dist/core/skill-tool-specifier.d.ts +1 -1
- package/dist/core/store-contracts/tool-result-store-contract.js +23 -0
- package/dist/core/task-notification.d.ts +2 -2
- package/dist/core/task-registry-agent.d.ts +9 -9
- package/dist/core/task-registry-shared.d.ts +4 -4
- package/dist/core/tighten-task-spec.js +18 -0
- package/dist/core/tool-errors.d.ts +2 -2
- package/dist/core/tool-policy.d.ts +64 -12
- package/dist/core/tool-policy.js +68 -8
- package/dist/core/tool-result-store.d.ts +2 -2
- package/dist/core/tool-result-store.js +3 -1
- package/dist/core/trace.d.ts +1 -1
- package/dist/core/types.d.ts +68 -13
- package/dist/core/untrusted-text.d.ts +6 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/engine/compaction/compaction.d.ts +1 -1
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/types.d.ts +12 -2
- package/dist/engine/llm/types.d.ts +1 -1
- package/dist/engine/loop/types.d.ts +2 -2
- package/dist/engine/session/import-validate.d.ts +1 -1
- package/dist/engine/session/log-digest.d.ts +1 -1
- package/dist/fixtures/index.d.ts +18 -2
- package/dist/fixtures/index.js +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/orchestration/goal.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +21 -0
- package/dist/orchestration/run-workflow-tool.js +6 -3
- package/dist/orchestration/workflow-primitives.d.ts +10 -1
- package/dist/orchestration/workflow-primitives.js +12 -1
- package/dist/orchestration/workflow-types.d.ts +5 -4
- package/dist/orchestration/workflow.js +11 -3
- package/dist/prompt-assembly/artifact.d.ts +1 -1
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/types.d.ts +4 -0
- package/dist/prompts/default.d.ts +14 -9
- package/dist/prompts/default.js +14 -4
- package/dist/scenarios/scenario-registry.d.ts +3 -3
- package/dist/stores/file/tool-result-store.d.ts +2 -2
- package/dist/tools/fs/bash-readonly-classifier.d.ts +23 -2
- package/dist/tools/fs/bash-readonly-classifier.js +11 -0
- package/dist/tools/fs/fs-bash.d.ts +8 -1
- package/dist/tools/fs/fs-bash.js +8 -3
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.js +2 -2
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +11 -7
- package/dist/tools/fs/fs-search-tools.d.ts +5 -3
- package/dist/tools/fs/fs-search-tools.js +15 -8
- package/dist/tools/fs/fs-shared.d.ts +6 -2
- package/dist/tools/fs/fs-shared.js +8 -3
- package/dist/tools/fs/index.d.ts +18 -0
- package/dist/tools/fs/index.js +13 -2
- package/dist/tools/fs/read-deny.d.ts +105 -0
- package/dist/tools/fs/read-deny.js +151 -0
- package/dist/tools/fs/read-face.d.ts +43 -0
- package/dist/tools/fs/read-face.js +38 -0
- package/dist/tools/fs/repo-map.d.ts +3 -1
- package/dist/tools/fs/repo-map.js +11 -5
- package/dist/tools/fs/safety.d.ts +34 -12
- package/dist/tools/fs/safety.js +88 -7
- package/dist/tools/fs/search.d.ts +54 -5
- package/dist/tools/fs/search.js +103 -21
- package/dist/tools/scheduler-tools.d.ts +1 -1
- package/dist/tools/task-list.d.ts +1 -1
- package/dist/tools/web.d.ts +3 -3
- package/package.json +1 -1
package/dist/core/scheduler.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ export declare class SchedulerError extends Error {
|
|
|
27
27
|
constructor(code: SchedulerErrorCode, message: string, cause?: unknown | undefined);
|
|
28
28
|
}
|
|
29
29
|
/**
|
|
30
|
-
* 🔴 **模型可控面(不可信)**:纯意图,**无任何身份字段** —— 防模型经自排 prompt
|
|
30
|
+
* 🔴 **模型可控面(不可信)**:纯意图,**无任何身份字段** —— 防模型经自排 prompt 权限抬升(design/62 同款红线:身份是
|
|
31
31
|
* Runner-held,非工具参数)。`prompt` = 未来 task 的不可信 `objective`(daemon 起 task 时经完整 prepare-task gate)。
|
|
32
32
|
*/
|
|
33
33
|
export interface ScheduledIntent {
|
|
@@ -84,7 +84,7 @@ export interface ScheduledIntent {
|
|
|
84
84
|
*/
|
|
85
85
|
export interface SchedulerContext {
|
|
86
86
|
/**
|
|
87
|
-
*
|
|
87
|
+
* 越界隔离键(cancel/list 只能操作同 scope 排的)。由 Runner 填,且**按被操作 intent 的 lifetime 分叉**
|
|
88
88
|
* (ruled 2026-08-03):`lifetime:"session"` 的意图用会话链(sessionId ?? principal ?? taskId ?? "default");
|
|
89
89
|
* durable 意图用跨会话稳定身份(principal ?? "default")——否则 durable job 会被钉死在排它的那个会话里,
|
|
90
90
|
* 后续会话 cancel/list 一律 `not_found`(任务照常触发却再也删不掉)。backend 侧无需分辨腿别:照旧只按
|
|
@@ -92,7 +92,7 @@ export interface SchedulerContext {
|
|
|
92
92
|
* ⚠️ 部署面:本分叉之前写下的 durable 行仍带旧的会话键,一次性重铸键归 backend 半场,engine 不做迁移。
|
|
93
93
|
*/
|
|
94
94
|
scope: string;
|
|
95
|
-
/** 排程者身份;daemon 触发时原样传给 runTask
|
|
95
|
+
/** 排程者身份;daemon 触发时原样传给 runTask(防权限抬升)。 */
|
|
96
96
|
principal?: string;
|
|
97
97
|
/** 可选:daemon 触发新 task 时复用的 session(chat-continuity;缺省=fresh-session)。⚠️ 复用需 durable sessionStore(design/105 §3.4)。 */
|
|
98
98
|
sessionId?: string;
|
|
@@ -169,7 +169,7 @@ export interface SchedulerCapability {
|
|
|
169
169
|
schedule(intent: ScheduledIntent, ctx: SchedulerContext): Promise<Result<{
|
|
170
170
|
id: ScheduledTaskId;
|
|
171
171
|
}, SchedulerError>>;
|
|
172
|
-
/** 取消一个已排意图(幂等:取消不存在的是 ok no-op)。🔴
|
|
172
|
+
/** 取消一个已排意图(幂等:取消不存在的是 ok no-op)。🔴 越界:`id` MUST 属本 scope;非本 scope 一律 `not_found`,不越界存在性。 */
|
|
173
173
|
cancel(id: ScheduledTaskId, ctx: SchedulerContext): Promise<Result<void, SchedulerError>>;
|
|
174
174
|
/** 列**本 scope** 已排意图(self-discovery + 防重复排)。范围 = `ctx.scope`,非全局。 */
|
|
175
175
|
list(ctx: SchedulerContext): Promise<Result<ScheduledTaskSummary[], SchedulerError>>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalizeTarget, writeTargetPath } from "../tools/fs/safety.js";
|
|
2
|
+
import { compileSegmentPattern, matchSegmentPatterns } from "../tools/fs/read-deny.js";
|
|
2
3
|
const DEFAULT_GUARDED_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
|
|
3
4
|
export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
4
5
|
".env",
|
|
@@ -25,33 +26,21 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
|
25
26
|
".bash_history",
|
|
26
27
|
".zsh_history",
|
|
27
28
|
];
|
|
28
|
-
function segmentGlobToRegExp(segment) {
|
|
29
|
-
const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*/g, "[^/]*");
|
|
30
|
-
return new RegExp(`^${escaped}$`, CASE_INSENSITIVE_FS ? "i" : "");
|
|
31
|
-
}
|
|
32
29
|
const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
|
|
33
30
|
function compilePatterns(patterns) {
|
|
34
31
|
const out = [];
|
|
35
32
|
for (const raw of patterns) {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
33
|
+
const compiled = compileSegmentPattern(raw, CASE_INSENSITIVE_FS ? "unicode" : "none");
|
|
34
|
+
if (compiled === null) {
|
|
38
35
|
throw new Error(`createSensitivePathPolicy: pattern ${JSON.stringify(raw)} contains no path segments and would guard nothing. ` +
|
|
39
36
|
`Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".git/hooks", "*.pem"); remove the entry or spell the segments.`);
|
|
40
37
|
}
|
|
41
|
-
out.push(
|
|
38
|
+
out.push(compiled);
|
|
42
39
|
}
|
|
43
40
|
return out;
|
|
44
41
|
}
|
|
45
42
|
function matchSensitive(canonicalKey, compiled) {
|
|
46
|
-
|
|
47
|
-
for (const pat of compiled) {
|
|
48
|
-
const n = pat.segments.length;
|
|
49
|
-
for (let i = 0; i + n <= segs.length; i++) {
|
|
50
|
-
if (pat.segments.every((re, j) => re.test(segs[i + j])))
|
|
51
|
-
return pat.raw;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
43
|
+
return matchSegmentPatterns(canonicalKey, compiled);
|
|
55
44
|
}
|
|
56
45
|
export function createSensitivePathPolicy(opts) {
|
|
57
46
|
const compiled = compilePatterns(opts.patterns);
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* can't lose an update (mirrors the checkpoint store's `resolve` OCC).
|
|
17
17
|
* - **structured fields only**: core interprets `toolAllow/Deny`, `allowDirs`, `commandAllow/Deny` (argv[0]
|
|
18
18
|
* names). A richer rule DSL is the service's/profile's job — core grows no rule interpreter.
|
|
19
|
-
* - **NOT A SANDBOX**: command rules inherit the coarse argv[0] matching (wrappers like `sh -c`/`sudo`
|
|
19
|
+
* - **NOT A SANDBOX**: command rules inherit the coarse argv[0] matching (wrappers like `sh -c`/`sudo` circumvention);
|
|
20
20
|
* the real isolation boundary is the executionEnv, never these rules.
|
|
21
21
|
*/
|
|
22
22
|
/** The structured per-session rules. All optional; an absent field imposes no constraint of that kind. */
|
|
@@ -86,11 +86,11 @@ export interface SessionPolicyStore {
|
|
|
86
86
|
/**
|
|
87
87
|
* LEXICAL path containment for the tighten gate. **Must agree with the runtime's view of what a dir IS** (the
|
|
88
88
|
* runtime resolves `..`/`.`/`//` via `canonicalizeTarget`): both args are `posix.normalize`d FIRST so a
|
|
89
|
-
* non-operator can't
|
|
89
|
+
* non-operator can't carry across a write-confinement OUTSIDE the prior dir by writing `<prior>/../etc` — which is
|
|
90
90
|
* lexically "within `<prior>`" by a naive `startsWith` but canonicalizes to `/etc` at run time (the tighten
|
|
91
91
|
* gate must never UNDER-report a loosening).
|
|
92
92
|
*
|
|
93
|
-
* The `..`-resolving `posix.normalize` pass is THIS function's own job (the tighten gate's `..`-
|
|
93
|
+
* The `..`-resolving `posix.normalize` pass is THIS function's own job (the tighten gate's `..`-carrying across
|
|
94
94
|
* defense above) and is NOT shared with the runtime's `isWithin` (session-rule-policy.ts), which compares
|
|
95
95
|
* already-canonicalized keys and never resolves `..` itself — the two functions agree on every OTHER input
|
|
96
96
|
* (same win-family fold, same prefix semantics) but diverge on inputs that still carry an unresolved `..`,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* WHY A GATE AT ALL. A TypeScript annotation is a compile-time claim, and a Provider is third-party
|
|
6
6
|
* (BYOM) code that may be plain JavaScript. Every value crossing this seam is therefore treated as
|
|
7
|
-
* foreign data: exotic shapes, throwing getters and
|
|
7
|
+
* foreign data: exotic shapes, throwing getters and untrusted metadata all have a defined verdict here,
|
|
8
8
|
* so no "shape outside the rules" can leak an exception into a tool body or a crafted string onto a
|
|
9
9
|
* prompt line.
|
|
10
10
|
*
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
* admitting a second program smuggled in behind the first.
|
|
36
36
|
*
|
|
37
37
|
* Reusing that parser rather than writing a second tokenizer is deliberate: two argv[0] parsers drift,
|
|
38
|
-
* and the drift shows up as a
|
|
38
|
+
* and the drift shows up as a circumvention rather than as a test failure.
|
|
39
39
|
*
|
|
40
40
|
* ## Scope: the shell-command lane only
|
|
41
41
|
*
|
|
@@ -55,5 +55,28 @@ export async function toolResultStoreContract(make, runAssertion) {
|
|
|
55
55
|
assert.equal((await store.get(unowned)).content, "bytes from a write site that stated no owner");
|
|
56
56
|
assert.equal(await ownerOf("tr_never~written"), undefined);
|
|
57
57
|
});
|
|
58
|
+
run("#196 deleteBySession (optional): four-state semantics when present; absence is REPORTED, not silently green", async () => {
|
|
59
|
+
const probe = make();
|
|
60
|
+
if (typeof probe.deleteBySession !== "function") {
|
|
61
|
+
assert.equal(probe.deleteBySession, undefined, "deleteBySession is NOT IMPLEMENTED on this backend — sweep semantics unverified (optional member; this is a disclosure, not a failure)");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const store = make();
|
|
65
|
+
const del = (sessionId) => Promise.resolve(store.deleteBySession(sessionId));
|
|
66
|
+
await store.put("tr_sweep~a", "A-bytes", { sessionId: "sess-X", taskId: "t1" });
|
|
67
|
+
await store.put("tr_sweep~b", "B-bytes", { sessionId: "sess-X" });
|
|
68
|
+
await store.put("tr_sweep~other", "OTHER-bytes", { sessionId: "sess-Y" });
|
|
69
|
+
await store.put("tr_sweep~unowned", "UNOWNED-bytes");
|
|
70
|
+
const first = await del("sess-X");
|
|
71
|
+
assert.equal(first.deleted, 2, "both rows recorded for the session are deleted (taskId presence does not split the key)");
|
|
72
|
+
assert.equal(await store.get("tr_sweep~a"), undefined, "a swept row is gone");
|
|
73
|
+
assert.equal(await store.get("tr_sweep~b"), undefined);
|
|
74
|
+
assert.equal((await store.get("tr_sweep~other")).content, "OTHER-bytes", "another session's row survives the sweep");
|
|
75
|
+
assert.equal(first.unattributable >= 1, true, "an unowned row is counted unattributable");
|
|
76
|
+
assert.equal((await store.get("tr_sweep~unowned")).content, "UNOWNED-bytes", "an unowned row is never deleted by a session sweep");
|
|
77
|
+
const second = await del("sess-X");
|
|
78
|
+
assert.equal(second.deleted, 0, "a repeated sweep deletes nothing");
|
|
79
|
+
assert.equal((await store.get("tr_sweep~other")).content, "OTHER-bytes");
|
|
80
|
+
});
|
|
58
81
|
await settle();
|
|
59
82
|
}
|
|
@@ -6,7 +6,7 @@ export interface TaskNotificationPayload {
|
|
|
6
6
|
task_id: string;
|
|
7
7
|
/** design/144 §2: `"external"` is CORE-MINTED ONLY — `TaskStream.notify()` casts it onto every
|
|
8
8
|
* externally injected event, and the input type ({@link ExternalNotificationInput}) deliberately has
|
|
9
|
-
* no `task_type` field, so an outside producer can never wear an internal lane's type (a
|
|
9
|
+
* no `task_type` field, so an outside producer can never wear an internal lane's type (a caller-declared
|
|
10
10
|
* "background_bash completed" impersonating a real child). */
|
|
11
11
|
task_type: "background_bash" | "workflow" | "background_agent" | "monitor" | "external";
|
|
12
12
|
toolUseId?: string;
|
|
@@ -27,7 +27,7 @@ export interface TaskNotificationPayload {
|
|
|
27
27
|
* `cycleSeq`, or — on a tier-3 cross-process revival / a parked resume, which deliberately carry
|
|
28
28
|
* no retain entry — the durable row's own `seq`. ABSENT only when neither exists (retain OFF on a
|
|
29
29
|
* store-less spawn, an expired ledger with no durable row, the fork lane): the cycle count is not
|
|
30
|
-
* knowable there, and an honest downgrade to the old single-cycle key beats a
|
|
30
|
+
* knowable there, and an honest downgrade to the old single-cycle key beats a caller-declared period
|
|
31
31
|
* number. So `seq` present does NOT imply a retain entry exists.
|
|
32
32
|
* - `external`: caller-supplied verbatim ({@link ExternalNotificationInput.seq}) — the injector's
|
|
33
33
|
* own dedup discriminator across same-task_id+status events.
|
|
@@ -14,14 +14,14 @@ export declare function ensureDurableHeartbeatLane(core: DurableAgentCore): void
|
|
|
14
14
|
* re-apply (the real terminal truth must beat a reaper's guess); only a row carrying a
|
|
15
15
|
* FOREIGN writerId poisons the lane (never clobber another writer). */
|
|
16
16
|
export declare function durableAgentWriteLane(handle: BackgroundAgentTaskHandle, patch: Partial<BackgroundAgentRecord>, clear?: readonly (keyof BackgroundAgentRecord)[]): void;
|
|
17
|
-
/** design/151 S1b — is this row's durable lane ARMED (store wired at registration, not
|
|
17
|
+
/** design/151 S1b — is this row's durable lane ARMED (store wired at registration, not contaminated)?
|
|
18
18
|
* Observability/test face. ⚠️ NOT the lifecycle-flip key: arming says a row was ATTEMPTED, not
|
|
19
19
|
* that it EXISTS — release anchors gate on {@link durableAgentRowProbe} (codex r2 F2). */
|
|
20
20
|
export declare function durableAgentArmedLane(core: DurableAgentCore, id: string): boolean;
|
|
21
21
|
/** design/151 S1b (codex r2 F2) — the AWAITABLE lifecycle-flip key. Returns undefined when no
|
|
22
22
|
* lane was armed; otherwise a probe CLOSURE over the HANDLE (outlives its eviction from the map —
|
|
23
23
|
* retain-ledger TTL arms fire hours after terminal GC) that flushes the queued writes and answers
|
|
24
|
-
* whether a durable row actually EXISTS right now (initial put confirmed, not
|
|
24
|
+
* whether a durable row actually EXISTS right now (initial put confirmed, not contaminated). A store
|
|
25
25
|
* that never accepted the put answers false — the caller then RELEASES the session (pre-151 path)
|
|
26
26
|
* instead of stranding an unreachable transcript.
|
|
27
27
|
*
|
|
@@ -102,13 +102,13 @@ export declare function bindBackgroundAgentSessionLane(core: DurableAgentCore, i
|
|
|
102
102
|
* per leg at the injector-ready barrier (prepare has succeeded by then, so the verdict exists); a
|
|
103
103
|
* revival cycle overwrites the previous cycle's record with its own, which the admission door
|
|
104
104
|
* guarantees is a subset. Same durable write lane as every other post-registration fact — a
|
|
105
|
-
* store-less registration is a no-op, and a
|
|
105
|
+
* store-less registration is a no-op, and a contaminated lane stays contaminated.
|
|
106
106
|
*
|
|
107
107
|
* A DROPPED write is disclosed. This record is the only carrier a narrowing has across a process
|
|
108
108
|
* boundary, so what stands on the row when the write does not land is the PREVIOUS leg's wider
|
|
109
|
-
* verdict — which the next revival then seeds from. Two drop shapes, one message each: a
|
|
109
|
+
* verdict — which the next revival then seeds from. Two drop shapes, one message each: a contaminated
|
|
110
110
|
* lane refuses at its front door, and an exhausted flush leaves the value unwritten with the lane
|
|
111
|
-
* still armed. The channel is the same durable-agents warning channel the lane uses for the
|
|
111
|
+
* still armed. The channel is the same durable-agents warning channel the lane uses for the contamination
|
|
112
112
|
* itself (that one says writes stopped; this one says WHICH fact stopped with them). The write
|
|
113
113
|
* remains best-effort: no leg faults because its bookkeeping failed.
|
|
114
114
|
*
|
|
@@ -197,7 +197,7 @@ export declare function rollbackParkedClaimLane(core: DurableAgentCore, stores:
|
|
|
197
197
|
* design/153 §7.2d (件3c, codex 3c H-1) — the CONSUME FLIP, SERIALIZED THROUGH THE DURABLE LANE:
|
|
198
198
|
* the guarded parked→running store CAS runs INSIDE the lane chain (ordered after every seeded
|
|
199
199
|
* write — the session-bind CAS included), so no in-flight lane write can race it, CAS-fail on the
|
|
200
|
-
* flip's rev bump, read the new epoch as foreign, and
|
|
200
|
+
* flip's rev bump, read the new epoch as foreign, and contaminate the lane (which would permanently
|
|
201
201
|
* drop finalize/terminal/re-park writes). On a won CAS the lane record/rev swap happens in the
|
|
202
202
|
* SAME chain step and the live handle adopts (running, arbitration dropped — the token is
|
|
203
203
|
* consumed; `parkClaimId` stays visible until finalize). Returns false when the row moved
|
|
@@ -284,7 +284,7 @@ export declare function markRetainedContinuationLane(core: DurableAgentCore, id:
|
|
|
284
284
|
* The DURABLE transition is a {@link claimTerminalRowForRevive} claim, awaited BEFORE the in-memory
|
|
285
285
|
* flip — the two orders are not equivalent: flipping memory first (the pre-arbitration shape, a plain lane
|
|
286
286
|
* write with no row guard) let a foreign claim win the row while this process kept serving
|
|
287
|
-
* `running`, and every later write of this cycle then died in the
|
|
287
|
+
* `running`, and every later write of this cycle then died in the contaminated lane without a trace.
|
|
288
288
|
* Three lane shapes, three treatments:
|
|
289
289
|
* - NO lane (store-less registration): the pre-arbitration in-memory-only transition, unchanged — there is
|
|
290
290
|
* no row, hence no arbitration domain and no competitor;
|
|
@@ -293,11 +293,11 @@ export declare function markRetainedContinuationLane(core: DurableAgentCore, id:
|
|
|
293
293
|
* the chain (row absent AND the put still unconfirmed after the chain drained), never by the
|
|
294
294
|
* entry-time flag: a put merely in flight, or one that committed under a failed response, would
|
|
295
295
|
* otherwise exempt a row that does exist — the split-brain window this claim exists to close;
|
|
296
|
-
* -
|
|
296
|
+
* - Contaminated lane (stale-flip ownership transfer / row reaped mid-write / heartbeat re-drive
|
|
297
297
|
* ceiling): the claim runs DIRECTLY through the store — the dead lane's chain is no longer a
|
|
298
298
|
* serialization point, the rev guard is. Re-arming that lane object in place is what is NOT
|
|
299
299
|
* done: an in-flight write from it would then see its own writerId on the re-claimed row and
|
|
300
|
-
* adopt it. It stays
|
|
300
|
+
* adopt it. It stays contaminated (its record keeps the pre-claim writerId/epoch, so its read-back
|
|
301
301
|
* answers "foreign" and it stops), and a row that cannot be claimed — gone, or live under
|
|
302
302
|
* another writer — REFUSES the revive rather than resuming with a durably unreachable row.
|
|
303
303
|
* A WON claim always hands the row to a FRESH lane for the claimed generation (both paths), for the
|
|
@@ -68,7 +68,7 @@ export interface UnifiedTaskOutput {
|
|
|
68
68
|
* (`noTask`/`noTaskForStop`/`formatWorkflowRun`/`serveDurableAgentRow`/`pollTask`/`stopTask` and every
|
|
69
69
|
* per-task-kind poll/stop arm). `UnifiedTaskOutput` (the `details` face) has no isError channel of its
|
|
70
70
|
* own — it is a REQUIRED-field wire shape consumed by callers beyond these two tools too, so isError is
|
|
71
|
-
* carried as a sibling on the WRAPPER instead (trap avoidance per the E1 sweep: never
|
|
71
|
+
* carried as a sibling on the WRAPPER instead (trap avoidance per the E1 sweep: never carry across isError
|
|
72
72
|
* through a narrower helper contract). Optional, absent/false = success — `createTaskOutputTool`/
|
|
73
73
|
* `createTaskStopTool` forward it onto the real `ToolReturn` at the tool boundary. Every internal helper
|
|
74
74
|
* keeps constructing `content`/`details` exactly as before; this only adds a place to say "and this one
|
|
@@ -97,8 +97,8 @@ export interface SemaTaskHandle {
|
|
|
97
97
|
/** P1-3(黑板 [1920]/[1921]/[1924]/[1925]) — mint a cross-channel completion correlation id ONCE per
|
|
98
98
|
* handle/record, fill-once (first writer wins): every settle/direct-terminal-flip site calls this
|
|
99
99
|
* instead of writing `completionId` directly, so no matter how many terminal-transition sites a lane
|
|
100
|
-
* has (settle / TaskStop
|
|
101
|
-
* (including an idempotent re-call from a
|
|
100
|
+
* has (settle / TaskStop circumvention / backfill), the value is minted exactly once and every later caller
|
|
101
|
+
* (including an idempotent re-call from a circumvention path that already ran) gets back the SAME id. `string`
|
|
102
102
|
* is intentionally loose (any object with an optional `completionId` field) so the same helper serves
|
|
103
103
|
* both a `RegisteredTaskHandle` and a `WorkflowRun` (orchestration/workflow.ts — a different module,
|
|
104
104
|
* no shared base type, same fill-once shape). */
|
|
@@ -834,7 +834,7 @@ export interface RegisterBackgroundAgentInput extends TaskAccess {
|
|
|
834
834
|
* pre-consume window keeps the live face's checkpoint arbitration; the consume flip
|
|
835
835
|
* {@link TaskRegistry.adoptParkedResume} is the only path to running). Pass the ticket's token
|
|
836
836
|
* and a stop-arbitration closure alongside. Also licenses replacing an in-process PARKED prior
|
|
837
|
-
* handle of the same id (the planned park→resume handoff; lane
|
|
837
|
+
* handle of the same id (the planned park→resume handoff; lane contaminated like the terminal replace). */
|
|
838
838
|
initialStatus?: "parked";
|
|
839
839
|
/** design/153 §7.2d — the parked checkpoint token this parked-born handle waits on. */
|
|
840
840
|
parkedCheckpointToken?: string;
|
|
@@ -25,6 +25,24 @@ export function tightenTaskSpec(base, overrides) {
|
|
|
25
25
|
merged[field] = [...new Set([...(b ?? []), ...(o ?? [])])];
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
if (base.readFace === "roots" && overrides.readFace === "open") {
|
|
29
|
+
throw new TaskSpecTightenError('tightenTaskSpec: cannot loosen readFace from "roots" to "open" (override may only tighten)');
|
|
30
|
+
}
|
|
31
|
+
if (base.readFace !== undefined || overrides.readFace !== undefined) {
|
|
32
|
+
merged.readFace = base.readFace === "roots" || overrides.readFace === "roots" ? "roots" : (overrides.readFace ?? base.readFace);
|
|
33
|
+
}
|
|
34
|
+
if (base.readDenyPatterns !== undefined || overrides.readDenyPatterns !== undefined) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const union = [];
|
|
37
|
+
for (const e of [...(base.readDenyPatterns ?? []), ...(overrides.readDenyPatterns ?? [])]) {
|
|
38
|
+
const key = typeof e === "string" ? `i:${e}` : `${e.caseSensitive === true ? "s" : "i"}:${e.pattern}`;
|
|
39
|
+
if (seen.has(key))
|
|
40
|
+
continue;
|
|
41
|
+
seen.add(key);
|
|
42
|
+
union.push(e);
|
|
43
|
+
}
|
|
44
|
+
merged.readDenyPatterns = union;
|
|
45
|
+
}
|
|
28
46
|
if (base.handsReadOnly === true && overrides.handsReadOnly === false) {
|
|
29
47
|
throw new TaskSpecTightenError("tightenTaskSpec: cannot loosen handsReadOnly from true to false (override may only tighten)");
|
|
30
48
|
}
|
|
@@ -77,7 +77,7 @@ export declare function formatToolError(error: unknown): string;
|
|
|
77
77
|
* in-presence condition) for that one code, or the errorCode's own doc for every other
|
|
78
78
|
* member (`env.lifetime_expired`'s doc says "retrying needs a NEW environment, not a
|
|
79
79
|
* smaller budget" — there is no `retryAfterMs` for it, by design: an expiring
|
|
80
|
-
* environment has no return time to give). Codex
|
|
80
|
+
* environment has no return time to give). Codex falsification review (rounds 2 and 4)
|
|
81
81
|
* twice suggested a distinct governance/platform class or leaving these `unknown`
|
|
82
82
|
* instead — rejected both times: (a) explicit calling-convention direction; (b)
|
|
83
83
|
* `WorkerErrorClass` is a small CLOSED public exported union — adding a member is an
|
|
@@ -86,7 +86,7 @@ export declare function formatToolError(error: unknown): string;
|
|
|
86
86
|
* terminal codes, not foreign ones); (c) pre-existing precedent (`config.compliance_required`,
|
|
87
87
|
* NOT part of this diff) already folds a transient/retryable governance code into `config`.
|
|
88
88
|
* `memory.admission_required`/`_denied`/`env.lifetime_expired` are EXACT codes, not broad
|
|
89
|
-
* `memory.`/`env.` prefixes — codex
|
|
89
|
+
* `memory.`/`env.` prefixes — codex falsification review (A-005.3 follow-up) caught an
|
|
90
90
|
* earlier draft folding EVERY `memory.*`/`env.*` code here, which misclassified the
|
|
91
91
|
* memory-engine's non-governance incident codes (`memory.secret_detected` et al.,
|
|
92
92
|
* explicitly listed as NON-governance in `NON_GOVERNANCE_MEMORY_CODES` — a content-safety
|
|
@@ -110,7 +110,7 @@ export type DecisionReason = (typeof DECISION_REASONS)[number];
|
|
|
110
110
|
* be reported as a person's refusal.
|
|
111
111
|
*
|
|
112
112
|
* ABSENT means this settlement named no source: an older caller that predates the field, or a verdict
|
|
113
|
-
* that settled no wait at all (a policy POSTURE — headless auto-deny, a blanket
|
|
113
|
+
* that settled no wait at all (a policy POSTURE — headless auto-deny, a blanket circumvention — where nobody
|
|
114
114
|
* was ever asked). A consumer MUST NOT read a semantic out of the absence; render the text instead.
|
|
115
115
|
*/
|
|
116
116
|
export type ApprovalSettledBy = "human" | "timeout" | "aborted";
|
|
@@ -129,7 +129,7 @@ export declare function isApprovalSettledBy(v: unknown): v is ApprovalSettledBy;
|
|
|
129
129
|
* stale un-redacted ones.
|
|
130
130
|
* - `ask` requests human confirmation; with no approver wired it resolves deterministically to deny
|
|
131
131
|
* (headless auto-deny, see `RunnerDeps.onAsk`). `requiresRealApproval: true` (RB-203 codex review,
|
|
132
|
-
* confirmed P1) additionally refuses to let a BLANKET `onAsk: "allow"`
|
|
132
|
+
* confirmed P1) additionally refuses to let a BLANKET `onAsk: "allow"` circumvention clear it — that
|
|
133
133
|
* configuration is "no approver looked at this at all", indistinguishable from headless, for a rule
|
|
134
134
|
* whose whole point is that a blanket policy setting must not be the thing that clears it (see
|
|
135
135
|
* {@link resolveAsk}). A live auto-mode classifier verdict (tried earlier in the gate, if configured)
|
|
@@ -175,6 +175,19 @@ export type PermissionResult = {
|
|
|
175
175
|
* consumer (approval card, wire frame) can tell the person their rule is alive, just outranked.
|
|
176
176
|
* Absent ⇒ no rule matched, or the ask was cleared normally. */
|
|
177
177
|
persistedRuleShadowed?: string;
|
|
178
|
+
/** An EXPLICIT `ask` permission rule matched this call (design/127 DSL — the CC `alwaysAskRules`
|
|
179
|
+
* shape); carries the matched rule's text. Present ⇔ a rule someone WROTE says "ask about this",
|
|
180
|
+
* never for a `defaultAction:"ask"` fallback (an unmatched call is default-closed posture, not a
|
|
181
|
+
* per-call instruction — CC draws the same line: only a matched ask rule diverts the auto-mode
|
|
182
|
+
* lane, a mode-default ask stays classifier-eligible). Consumer: the gate's auto-mode classifier
|
|
183
|
+
* step refuses to resolve an ask carrying it — a person's standing "ask me each time" is not
|
|
184
|
+
* classifier hesitation, so a classifier verdict must not be the thing that clears it. A policy
|
|
185
|
+
* self-declaring the member only ever OPTS ITS OWN ask OUT of classifier auto-resolution
|
|
186
|
+
* (tightening — the false-claim direction is safe, unlike the engine-stamped `"hook"` word whose
|
|
187
|
+
* risk was laundering provenance AWAY). {@link combinePolicies} carries it onto the surviving ask
|
|
188
|
+
* when ANY folded-away concurrent ask bore it (monotone, tighten-only); it is consumed inside the
|
|
189
|
+
* gate and deliberately NOT copied onto the park/approval-card request. */
|
|
190
|
+
matchedAskRule?: string;
|
|
178
191
|
} | {
|
|
179
192
|
action: "deny";
|
|
180
193
|
updatedInput?: unknown;
|
|
@@ -356,9 +369,9 @@ export declare function constraintChainDigest(chain: readonly ConstraintChainEnt
|
|
|
356
369
|
* STRUCTURALLY unstable carriers — getters, prototype-supplied values — i.e. shapes an ordinary
|
|
357
370
|
* deployment can write by accident. It does NOT try to defeat a Proxy whose descriptor trap reports a
|
|
358
371
|
* data property while its get trap stays stateful: same-process JavaScript has no trust boundary a
|
|
359
|
-
* function can enforce (an
|
|
372
|
+
* function can enforce (an untrusted party who ships such a Proxy can as easily patch this module), so
|
|
360
373
|
* chasing that shape adds complexity without adding a guarantee. The screen's promise is against
|
|
361
|
-
* drift and accident, not against a
|
|
374
|
+
* drift and accident, not against a untrusted co-resident.
|
|
362
375
|
*
|
|
363
376
|
* `reasonIsNonInput` (the HOOK seam's spelling — merged-code scan, 5.28 window): at that seam the
|
|
364
377
|
* field is documented as DISCARDED — every mint point downstream unconditionally re-stamps
|
|
@@ -475,14 +488,14 @@ export declare const COARSE_SHELL_TOOLS: string[];
|
|
|
475
488
|
* design/center §10 — a COARSE allow/deny gate keyed on the LEADING command NAME (`argv[0]`) of a single
|
|
476
489
|
* simple shell command. It reuses the ONE shared simple-command parser
|
|
477
490
|
* ({@link import("../tools/fs/index.js").parseLeadingCommandName}) — it does NOT re-implement argv[0] parsing
|
|
478
|
-
* (a second parser would drift and open a
|
|
491
|
+
* (a second parser would drift and open a circumvention). For a `bash`/`bash_readonly` call it extracts the leading
|
|
479
492
|
* command name and:
|
|
480
493
|
* - if the name is in `deny` → `deny` (deny-wins, matching {@link createAllowDenyPolicy}; NOT last-match-wins,
|
|
481
494
|
* which would fight {@link combinePolicies}'s deny>ask>allow fold);
|
|
482
495
|
* - else if `allow` is provided and the name is NOT in it → `defaultAction` (`"ask"` default, or `"deny"`);
|
|
483
496
|
* - else → `allow`.
|
|
484
497
|
* A command the parser cannot reduce to a single bare name (it has shell operators / a path-prefix / a leading
|
|
485
|
-
* env-assignment) is treated by `defaultAction` (fail-toward-gate): such a command is exactly what would
|
|
498
|
+
* env-assignment) is treated by `defaultAction` (fail-toward-gate): such a command is exactly what would circumvent
|
|
486
499
|
* an argv[0] filter, so it should not silently `allow`. NON-shell tools are out of scope → `allow` (this gate
|
|
487
500
|
* only speaks about shell command names; compose it with other policies for the rest).
|
|
488
501
|
*
|
|
@@ -490,7 +503,7 @@ export declare const COARSE_SHELL_TOOLS: string[];
|
|
|
490
503
|
* injected by the profile/config/deployment.
|
|
491
504
|
*
|
|
492
505
|
* ⚠️ **NOT A SANDBOX — coarse filter / defense-in-depth only.** This matches `argv[0]` NAME and nothing else,
|
|
493
|
-
* so the
|
|
506
|
+
* so the circumvention surface is large and the gate MUST NOT be relied on as containment:
|
|
494
507
|
* - command substitution / subshells (`$(curl …)`, backticks), pipes, redirects, `;`/`&&` chaining — all
|
|
495
508
|
* rejected as un-parseable here (→ `defaultAction`), so they don't sneak past `allow`, but that is a GATE
|
|
496
509
|
* not a guarantee of safety;
|
|
@@ -574,7 +587,7 @@ export declare function findUnverifiableRecursiveDelete(command: string, safeVar
|
|
|
574
587
|
*
|
|
575
588
|
* codex review (confirmed P1): the one piece of NEW plumbing this DOES need — `requiresRealApproval: true`
|
|
576
589
|
* (see {@link PermissionResult}'s `ask` doc) — closes a real gap the first cut of this fix missed. A
|
|
577
|
-
* deployment using the documented `onAsk: "allow"`
|
|
590
|
+
* deployment using the documented `onAsk: "allow"` circumvention mode (sandbox-is-the-boundary posture, the
|
|
578
591
|
* shape a sandboxed autonomy container runs) would otherwise have this rule's `ask` rubber-stamped by that blanket
|
|
579
592
|
* configuration with NO classifier ever consulted and NO judgment of any kind applied — silently WEAKER
|
|
580
593
|
* than the unconditional deny it replaced, for exactly the deployments that opted into the most permissive
|
|
@@ -702,7 +715,7 @@ export interface AskRequest {
|
|
|
702
715
|
readonly sourceTaskId?: string;
|
|
703
716
|
/** design/153 §1 (RB-39②) — Runner-filled: present (`true`) exactly when the gate raising this ask
|
|
704
717
|
* belongs to a DELEGATED SUBAGENT (the prepareTask `isSubagent` fact — trusted `internals.parentTaskId`
|
|
705
|
-
* presence; a child cannot
|
|
718
|
+
* presence; a child cannot self-declare or suppress it, internals never ride a TaskSpec). Undefined ⇒ the
|
|
706
719
|
* host / a top-level task. THE explicit "from a background agent" discriminator (replaces presence-
|
|
707
720
|
* or equality-heuristics on {@link sourceTaskId}). `readonly`, sync-path-only, S1d posture. */
|
|
708
721
|
readonly fromSubagent?: true;
|
|
@@ -711,6 +724,26 @@ export interface AskRequest {
|
|
|
711
724
|
* (the spawning model chooses names): redact + bound before rendering; never adjudication input.
|
|
712
725
|
* `readonly`, same posture as {@link fromSubagent}. */
|
|
713
726
|
readonly sourceAgentName?: string;
|
|
727
|
+
/** Runner-filled: present (`true`) exactly when the issuing run is a **delegated child** — ANY
|
|
728
|
+
* spawn leg, FORK INCLUDED (the trusted `RunInternals.isDelegatedChild` fact, set unconditionally
|
|
729
|
+
* by every core spawn path). It selects which REFUSAL POSTURE a human deny speaks back to this
|
|
730
|
+
* run's model: a parent-thread run is told to stop and wait for the user (its transcript has a
|
|
731
|
+
* user turn coming), a delegated child is told to adapt or report the limitation. A fork is
|
|
732
|
+
* deliberately IN: it inherits the parent's authority (design/110 — which is why the RB-330
|
|
733
|
+
* `isDelegatedNonForkChild` derivation, serving the authority/context faces, excludes it), but
|
|
734
|
+
* its interaction contract is one-shot — "report once and stop … no waiting for the user"
|
|
735
|
+
* (FORK_DIRECTIVE_FRAME) — so a stop-and-wait refusal would instruct it to do the impossible
|
|
736
|
+
* (codex adversarial round, confirmed). NOT the same fact as {@link fromSubagent} either: that is
|
|
737
|
+
* the design/153 attribution discriminator (`parentTaskId` presence), which a directly-started
|
|
738
|
+
* workflow's children lack even though refusals must still speak the child posture to them.
|
|
739
|
+
* `readonly`, trusted (internals never ride a TaskSpec), sync-path-only like its siblings.
|
|
740
|
+
*
|
|
741
|
+
* Key division, at a glance (the two child-ish keys serve DIFFERENT faces — neither implies the other):
|
|
742
|
+
* - `fromSubagent` — ATTRIBUTION: "a nameable parent task issued this" (`parentTaskId` presence);
|
|
743
|
+
* drives display/identity surfaces.
|
|
744
|
+
* - `isDelegatedChild` — REFUSAL POSTURE: "no user turn ever lands in this run's transcript";
|
|
745
|
+
* drives which deny text the model is told (stop-and-wait vs adapt-or-report), forks included. */
|
|
746
|
+
readonly isDelegatedChild?: true;
|
|
714
747
|
/** RB-203 (codex review, confirmed P1) — carried from the originating {@link PermissionResult}'s ask
|
|
715
748
|
* variant of the same name: `true` ⇒ {@link resolveAsk} must not let a blanket `onAsk: "allow"`
|
|
716
749
|
* clear this ask (see that field's doc). `readonly`, filled by the gate from the decision, never a
|
|
@@ -762,7 +795,7 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
762
795
|
* untyped bridge is a fail-closed deny naming the defect (the historical truthy leniency was a
|
|
763
796
|
* fail-open on the security face with no live producer).
|
|
764
797
|
* - `"unavailable"` — the G1 per-ask routing verdict (see {@link OnAsk}).
|
|
765
|
-
* - `{ allow, updatedInput?, settledBy? }` — verdict PLUS an operator EDIT of the presented args
|
|
798
|
+
* - `{ allow, updatedInput?, settledBy?, reason? }` — verdict PLUS an operator EDIT of the presented args
|
|
766
799
|
* (whole-replacement form, e.g. ctrl+g "edit script in $EDITOR"): the human approved a MODIFIED
|
|
767
800
|
* action, and executing the un-edited args would betray that consent. `allow` folds STRICTLY
|
|
768
801
|
* (`allow === true`) — an object arm is a deliberate caller, so no truthy leniency — and
|
|
@@ -783,11 +816,30 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
|
|
|
783
816
|
* `"aborted"` is deliberately NOT accepted here — that word names the engine's OWN fail-closed ends
|
|
784
817
|
* (abort, throw, unavailable, out-of-contract value), each already stamped at its own arm, and a
|
|
785
818
|
* self-reported one would let a host relabel its refusal as an engine failure.
|
|
819
|
+
*
|
|
820
|
+
* `reason` — the SYNCHRONOUS leg's seat for a model-readable reason attached to a deny, the exact
|
|
821
|
+
* counterpart of the durable leg's `ResumeOutcome` `policy_ask` `reason` ("Model-readable reason
|
|
822
|
+
* attached to a deny"): the words the decider typed with their "no" (an approval card's rejection
|
|
823
|
+
* input), relayed to the model so it can follow the person's direction instead of guessing. In-place
|
|
824
|
+
* conditions, all screened at {@link resolveAsk}:
|
|
825
|
+
* - meaningful ONLY on a deny (`allow: false`); on an allow the seat is NEVER READ (accepted and
|
|
826
|
+
* unused, literally — codex round: a junk value or a throwing accessor riding an approval cannot
|
|
827
|
+
* destroy the approval). Same net posture as the durable leg, where a `reason` on an `allow` row
|
|
828
|
+
* is never rendered;
|
|
829
|
+
* - UNTRUSTED input (a human's free text riding a callback): it reaches the model only inside a
|
|
830
|
+
* `delimitUntrusted` fence, the same containment the durable resume's reviewer note gets, and
|
|
831
|
+
* bounded to `REVIEWER_NOTE_MAX_BODY` code points (an oversized note arrives truncated with an
|
|
832
|
+
* ellipsis, never verbatim and never silently dropped — both legs share the one bound);
|
|
833
|
+
* - on a deny, a present non-string is refused loudly (fail-closed deny naming the defect — the
|
|
834
|
+
* domain the durable leg enforces pre-CAS for the same field), a THROWING read is a fail-closed
|
|
835
|
+
* deny naming the true cause (never a raw rejection out of the gate), and the empty string reads
|
|
836
|
+
* as absent (truthiness, the durable consumer's own read).
|
|
786
837
|
*/
|
|
787
838
|
export type AskOutcome = boolean | "unavailable" | {
|
|
788
839
|
allow: boolean;
|
|
789
840
|
updatedInput?: unknown;
|
|
790
841
|
settledBy?: Extract<ApprovalSettledBy, "human" | "timeout">;
|
|
842
|
+
reason?: string;
|
|
791
843
|
};
|
|
792
844
|
/**
|
|
793
845
|
* ruled 2026-08-04 — forward an approver into a delegated child, stamping every ask it raises with the
|
|
@@ -834,7 +886,7 @@ export declare function tryCloneArgs<T>(v: T): {
|
|
|
834
886
|
};
|
|
835
887
|
/**
|
|
836
888
|
* A label for a value that was THROWN at us, safe to put in a fail-closed refusal. Never coerces: `String(x)`
|
|
837
|
-
* throws on a null-prototype object or a
|
|
889
|
+
* throws on a null-prototype object or a untrusted `toString`/`Symbol.toPrimitive`, and the thrower here is
|
|
838
890
|
* deployment-supplied (a getter inside the args can throw any object at `structuredClone`, an approver can
|
|
839
891
|
* throw one at the gate). A coercion failure inside the catch would escape `resolveAsk` as a raw TypeError —
|
|
840
892
|
* turning a classified fail-closed DENY into an unclassified crash, i.e. the failing input escaping the very
|
|
@@ -851,7 +903,7 @@ export declare function tryCloneArgs<T>(v: T): {
|
|
|
851
903
|
*
|
|
852
904
|
* Exported for the durable half of the same boundary (the runner's park mint), which now has a SECOND
|
|
853
905
|
* way a deployment-supplied value can throw at capture time — the target backend's own codec refusing to
|
|
854
|
-
* encode it. One containment point, so the two refusals cannot differ on how much of a
|
|
906
|
+
* encode it. One containment point, so the two refusals cannot differ on how much of a untrusted value's
|
|
855
907
|
* text they let through. Not part of the package's public surface.
|
|
856
908
|
*/
|
|
857
909
|
export declare function describeThrown(err: unknown): string;
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -4,7 +4,7 @@ import { brandPolicyAskClass } from "./ask-class.js";
|
|
|
4
4
|
import { join, normalize as normalizePath, posix as posixPath, sep, win32 as winPath } from "node:path";
|
|
5
5
|
import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
|
|
6
6
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
7
|
-
import { inlineUntrusted } from "./untrusted-text.js";
|
|
7
|
+
import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
|
|
8
8
|
import { parsePermissionRule } from "./permission-rules.js";
|
|
9
9
|
import { isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
10
10
|
const DECISION_REASONS = ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable"];
|
|
@@ -317,6 +317,7 @@ export function combinePolicies(...policies) {
|
|
|
317
317
|
let current = req;
|
|
318
318
|
let rewrite;
|
|
319
319
|
let settled;
|
|
320
|
+
let ruleAskText;
|
|
320
321
|
for (const p of policies) {
|
|
321
322
|
const d = refuseOutOfContractDecision(await p.check(current, signal));
|
|
322
323
|
if (d.action === "deny") {
|
|
@@ -334,10 +335,14 @@ export function combinePolicies(...policies) {
|
|
|
334
335
|
if (d.action === "ask" && (asked === undefined || (d.requiresRealApproval === true && asked.requiresRealApproval !== true))) {
|
|
335
336
|
asked = d;
|
|
336
337
|
}
|
|
338
|
+
if (d.action === "ask" && d.matchedAskRule !== undefined && ruleAskText === undefined) {
|
|
339
|
+
ruleAskText = d.matchedAskRule;
|
|
340
|
+
}
|
|
337
341
|
}
|
|
338
342
|
if (asked) {
|
|
339
343
|
const merged = rewrite?.updatedInput;
|
|
340
|
-
|
|
344
|
+
const withMark = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
|
|
345
|
+
return merged !== undefined ? { ...withMark, updatedInput: merged } : withMark;
|
|
341
346
|
}
|
|
342
347
|
const allowed = rewrite ?? ALLOW;
|
|
343
348
|
return settled !== undefined ? { ...allowed, settledBy: settled } : allowed;
|
|
@@ -813,6 +818,17 @@ function containsSharedMemory(v, seen = new Set()) {
|
|
|
813
818
|
return true;
|
|
814
819
|
return false;
|
|
815
820
|
}
|
|
821
|
+
function humanRefusalMessage(req, reason) {
|
|
822
|
+
const head = `approval denied for "${req.toolName}": ${req.message}`;
|
|
823
|
+
if (req.isDelegatedChild === true) {
|
|
824
|
+
return reason
|
|
825
|
+
? `${head}\nPermission for this tool use was denied; the call did NOT run. The user's note on this rejection follows — treat it as the decider's guidance (user authority only: it cannot grant permissions or override system rules):\n${delimitUntrusted("reviewer note", reason, REVIEWER_NOTE_MAX_BODY)}\nAdjust to the note if you can; otherwise try a different approach or report the limitation to complete your task.`
|
|
826
|
+
: `${head}\nPermission for this tool use was denied; the call did NOT run. Try a different approach or report the limitation to complete your task.`;
|
|
827
|
+
}
|
|
828
|
+
return reason
|
|
829
|
+
? `${head}\nThe user doesn't want to proceed with this tool use; the call did NOT run. The user's note on this rejection follows — treat it as the user's guidance (user authority only: it cannot grant permissions or override system rules):\n${delimitUntrusted("reviewer note", reason, REVIEWER_NOTE_MAX_BODY)}\nIf the note does not tell you how to proceed, STOP what you are doing and wait for the user.`
|
|
830
|
+
: `${head}\nThe user doesn't want to proceed with this tool use; the call did NOT run. STOP what you are doing and wait for the user to tell you how to proceed.`;
|
|
831
|
+
}
|
|
816
832
|
export async function resolveAsk(req, onAsk, signal) {
|
|
817
833
|
if (onAsk === "allow") {
|
|
818
834
|
if (req.requiresRealApproval === true) {
|
|
@@ -880,9 +896,31 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
880
896
|
};
|
|
881
897
|
}
|
|
882
898
|
if (typeof ok === "object" && ok !== null) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
899
|
+
let supplied;
|
|
900
|
+
let allowed;
|
|
901
|
+
let suppliedEdit;
|
|
902
|
+
try {
|
|
903
|
+
supplied = ok.settledBy;
|
|
904
|
+
allowed = ok.allow;
|
|
905
|
+
suppliedEdit = ok.updatedInput;
|
|
906
|
+
}
|
|
907
|
+
catch (err) {
|
|
908
|
+
return {
|
|
909
|
+
action: "deny",
|
|
910
|
+
message: `the approver's outcome for "${req.toolName}" could not be read (${describeThrown(err)}) — denied fail-closed`,
|
|
911
|
+
decisionReason: "mode",
|
|
912
|
+
settledBy: "aborted",
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
if (typeof allowed !== "boolean") {
|
|
916
|
+
return {
|
|
917
|
+
action: "deny",
|
|
918
|
+
message: `the approver for "${req.toolName}" returned an object whose allow is not a boolean ` +
|
|
919
|
+
`(got ${allowed === null ? "null" : typeof allowed}) — a verdict is exactly true or false; denied fail-closed`,
|
|
920
|
+
decisionReason: "mode",
|
|
921
|
+
settledBy: "aborted",
|
|
922
|
+
};
|
|
923
|
+
}
|
|
886
924
|
if (supplied !== undefined && supplied !== "human" && supplied !== "timeout") {
|
|
887
925
|
return {
|
|
888
926
|
action: "deny",
|
|
@@ -902,11 +940,33 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
902
940
|
};
|
|
903
941
|
}
|
|
904
942
|
if (allowed !== true) {
|
|
943
|
+
let reasonText;
|
|
944
|
+
try {
|
|
945
|
+
const suppliedReason = ok.reason;
|
|
946
|
+
if (suppliedReason !== undefined && typeof suppliedReason !== "string") {
|
|
947
|
+
return {
|
|
948
|
+
action: "deny",
|
|
949
|
+
message: `the approver for "${req.toolName}" attached a reason that is not a plain string ` +
|
|
950
|
+
`(got ${suppliedReason === null ? "null" : typeof suppliedReason}) — a deny note is the decider's plain text; denied fail-closed`,
|
|
951
|
+
decisionReason: "mode",
|
|
952
|
+
settledBy: "aborted",
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
reasonText = typeof suppliedReason === "string" && suppliedReason !== "" ? suppliedReason : undefined;
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
return {
|
|
959
|
+
action: "deny",
|
|
960
|
+
message: `the approver for "${req.toolName}" attached a reason that could not be read (${describeThrown(err)}) — denied fail-closed`,
|
|
961
|
+
decisionReason: "mode",
|
|
962
|
+
settledBy: "aborted",
|
|
963
|
+
};
|
|
964
|
+
}
|
|
905
965
|
return {
|
|
906
966
|
action: "deny",
|
|
907
967
|
message: supplied === "timeout"
|
|
908
|
-
? `approval for "${req.toolName}" was not answered before the approver's own window elapsed: ${req.message}`
|
|
909
|
-
:
|
|
968
|
+
? `approval for "${req.toolName}" was not answered before the approver's own window elapsed: ${req.message}${reasonText !== undefined ? `\nReviewer note:\n${delimitUntrusted("reviewer note", reasonText, REVIEWER_NOTE_MAX_BODY)}` : ""}`
|
|
969
|
+
: humanRefusalMessage(req, reasonText),
|
|
910
970
|
decisionReason: "mode",
|
|
911
971
|
settledBy: supplied === "timeout" ? "timeout" : "human",
|
|
912
972
|
};
|
|
@@ -935,5 +995,5 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
935
995
|
settledBy: "aborted",
|
|
936
996
|
};
|
|
937
997
|
}
|
|
938
|
-
return { action: "deny", message:
|
|
998
|
+
return { action: "deny", message: humanRefusalMessage(req), decisionReason: "mode", settledBy: "human" };
|
|
939
999
|
}
|