@sema-agent/server 6.8.0 → 7.1.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/dist/boot/org-memory.d.ts +36 -0
- package/dist/boot/org-memory.js +76 -0
- package/dist/boot/resolve-spec.d.ts +6 -0
- package/dist/boot/resolve-spec.js +41 -11
- package/dist/boot/runner-deps.d.ts +6 -2
- package/dist/boot/runner-deps.js +11 -2
- package/dist/config-center/http-client.d.ts +1 -0
- package/dist/config-center/http-client.js +38 -14
- package/dist/config-types.d.ts +16 -1
- package/dist/config.js +18 -1
- package/dist/fleet/fleet-bus.d.ts +9 -0
- package/dist/fleet/fleet-bus.js +10 -0
- package/dist/http/routes/tasks.js +11 -3
- package/dist/http/server.js +8 -5
- package/dist/main.js +6 -1
- package/dist/memory-scope.d.ts +10 -1
- package/dist/memory-scope.js +22 -2
- package/dist/org-memory-admission.d.ts +96 -0
- package/dist/org-memory-admission.js +224 -0
- package/dist/question.d.ts +5 -0
- package/dist/question.js +7 -0
- package/dist/runs.d.ts +7 -0
- package/dist/runs.js +17 -3
- package/dist/runtime-governance.d.ts +14 -6
- package/dist/runtime-governance.js +27 -6
- package/dist/task-settings.d.ts +10 -8
- package/dist/task-settings.js +7 -19
- package/package.json +4 -4
package/dist/memory-scope.d.ts
CHANGED
|
@@ -58,9 +58,18 @@ export declare function memoryEngineBackendFor(config: ServiceConfig, fallbackRo
|
|
|
58
58
|
* (memory feature off for this run). Pure (testable in isolation) — main.ts's resolveSpec composes it with the
|
|
59
59
|
* engine-backend presence guard.
|
|
60
60
|
*/
|
|
61
|
-
export declare function memorySpecForRequest(scope: string | undefined, memoryWrite: boolean | undefined, defaultScopes?: string[]
|
|
61
|
+
export declare function memorySpecForRequest(scope: string | undefined, memoryWrite: boolean | undefined, defaultScopes?: string[],
|
|
62
|
+
/** design/170 件A(#148 件3④):origin 盖章的部署形态维(N2)。`multiTenant=true`(requirePrincipal)
|
|
63
|
+
* ⇒ 登记簿 defaultScopes 的 org 键按 **request** 盖章(条目由调用方 projectId 选定=caller 可影响的
|
|
64
|
+
* 选择器,core 准入门据此过目录判决);单用户 ⇒ 一律 deployment(operator 登记簿条目归 deployment,
|
|
65
|
+
* 否则单用户部署被自家规则整拒——v4 §0 伤害①的成立前提)。缺参=旧调用形,不盖章(整键缺席=
|
|
66
|
+
* core legacy 语义,零迁移)。 */
|
|
67
|
+
originPolicy?: {
|
|
68
|
+
multiTenant: boolean;
|
|
69
|
+
}): {
|
|
62
70
|
scopes: string[];
|
|
63
71
|
writeScope?: string | null;
|
|
64
72
|
scopeContract?: "v2";
|
|
73
|
+
scopeOrigins?: Record<string, "deployment" | "request">;
|
|
65
74
|
} | undefined;
|
|
66
75
|
//# sourceMappingURL=memory-scope.d.ts.map
|
package/dist/memory-scope.js
CHANGED
|
@@ -98,7 +98,13 @@ export function memoryEngineBackendFor(config, fallbackRoot) {
|
|
|
98
98
|
* (memory feature off for this run). Pure (testable in isolation) — main.ts's resolveSpec composes it with the
|
|
99
99
|
* engine-backend presence guard.
|
|
100
100
|
*/
|
|
101
|
-
export function memorySpecForRequest(scope, memoryWrite, defaultScopes
|
|
101
|
+
export function memorySpecForRequest(scope, memoryWrite, defaultScopes,
|
|
102
|
+
/** design/170 件A(#148 件3④):origin 盖章的部署形态维(N2)。`multiTenant=true`(requirePrincipal)
|
|
103
|
+
* ⇒ 登记簿 defaultScopes 的 org 键按 **request** 盖章(条目由调用方 projectId 选定=caller 可影响的
|
|
104
|
+
* 选择器,core 准入门据此过目录判决);单用户 ⇒ 一律 deployment(operator 登记簿条目归 deployment,
|
|
105
|
+
* 否则单用户部署被自家规则整拒——v4 §0 伤害①的成立前提)。缺参=旧调用形,不盖章(整键缺席=
|
|
106
|
+
* core legacy 语义,零迁移)。 */
|
|
107
|
+
originPolicy) {
|
|
102
108
|
if (!scope)
|
|
103
109
|
return undefined;
|
|
104
110
|
// 142-S1.5: a scope carrying a v2 typed prefix rides WITH the explicit contract marker — core validates
|
|
@@ -119,7 +125,21 @@ export function memorySpecForRequest(scope, memoryWrite, defaultScopes) {
|
|
|
119
125
|
// 最后一层就是派生 scope=正确,不钉保持最小形)。
|
|
120
126
|
const layered = extras.length > 0 ? { scopes: [scope, ...extras], writeScope: scope } : { scopes: [scope] };
|
|
121
127
|
const v2 = isV2(scope) || extras.some(isV2) ? { scopeContract: "v2" } : {};
|
|
128
|
+
// 件A origin 盖章(core 5.13.0 `memory.scopeOrigins` 两格语义):只盖 org 键(非 org 不入准入判决,
|
|
129
|
+
// v4 §1);派生/env 位的 `scope` org 形只可能来自 operator 显式 MEMORY_SCOPE(memoryScopeFor 多租户
|
|
130
|
+
// 恒铸 user: 形)⇒ deployment;登记簿种子按部署形态维分格(见参数注)。零 org 键 ⇒ 整键缺席
|
|
131
|
+
// (core legacy 零迁移;非 org 部署形状逐字节不变=additive 锁)。
|
|
132
|
+
const stamped = {};
|
|
133
|
+
if (originPolicy !== undefined) {
|
|
134
|
+
if (scope.startsWith("org:"))
|
|
135
|
+
stamped[scope] = "deployment";
|
|
136
|
+
for (const extra of extras) {
|
|
137
|
+
if (extra.startsWith("org:"))
|
|
138
|
+
stamped[extra] = originPolicy.multiTenant ? "request" : "deployment";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const origins = Object.keys(stamped).length > 0 ? { scopeOrigins: stamped } : {};
|
|
122
142
|
// memoryWrite === false(MF-30 pause)wins over the layered writeScope pin — 只读语义在有种子时同样成立。
|
|
123
|
-
return memoryWrite === false ? { ...layered, writeScope: null, ...v2 } : { ...layered, ...v2 };
|
|
143
|
+
return memoryWrite === false ? { ...layered, writeScope: null, ...v2, ...origins } : { ...layered, ...v2, ...origins };
|
|
124
144
|
}
|
|
125
145
|
//# sourceMappingURL=memory-scope.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org 记忆准入 resolver — server 侧对接 @sema-agent/core@5.13.0 `RunnerDeps.memoryScopeAdmission` seam。
|
|
3
|
+
* 授权事实来自「目录」抽象:远程腿 = config-center per-principal caps 响应里的 `orgMemory` 段(形状
|
|
4
|
+
* 同源锚 = registry-core `PrincipalOrgMemoryWire`,zod schema 直接 safeParse);单机腿 = env JSON 静态表
|
|
5
|
+
* (`MEMORY_ORG_DIRECTORY_JSON`,{@link parseOrgDirectoryStatic} 装载,启动期 fail-loud)。
|
|
6
|
+
*
|
|
7
|
+
* 判别联合 {@link OrgDirectoryLookup} 是本模块的核心裁定(设计 N3/C5):取数成功(granted,含零授权空表
|
|
8
|
+
* = 负结果)与瞬时不可用(unavailable)绝不塌成一格——granted 空表是 resolver 可以终局判决的「事实」
|
|
9
|
+
* (「这个 principal 没有这个 scope」),unavailable 必须让 core 铸瞬时码 memory.admission_required 重试,
|
|
10
|
+
* 而不是被误判成「没这个 scope」而永久拒绝。
|
|
11
|
+
*
|
|
12
|
+
* core 侧契约(RunnerDeps.memoryScopeAdmission,core types.d.ts):resolver 返回 {ok:false} ⇒ core 铸终局码
|
|
13
|
+
* memory.admission_denied;resolver throw 一个带 `retryAfterMs`(ms)属性的 Error ⇒ core 铸瞬时码
|
|
14
|
+
* memory.admission_required 并透传 retryAfterMs;{ok:true}.scopes 只能 ⊆ requested(多给 = resolver fault
|
|
15
|
+
* 会被 core 整拒)。core 只在「存在 request-origin org scope 且 principal 缺席」时自己先拒——本模块仍防御
|
|
16
|
+
* `principal === undefined` 这一支(不能信任「core 一定先拒过」这件事)。
|
|
17
|
+
*/
|
|
18
|
+
import type { MemoryScopeAdmission } from "@sema-agent/core";
|
|
19
|
+
/**
|
|
20
|
+
* 单机腿:装载 `MEMORY_ORG_DIRECTORY_JSON`(`Record<principal, Record<orgScope, {write?}>>`)。启动期调用,
|
|
21
|
+
* **fail-loud**——任何非法直接 throw 且消息点名坏在哪个键(响亮拒是正确方向:一张半坏的授权表比拒绝
|
|
22
|
+
* 启动更危险)。principal 键复用 {@link assertPrincipalShape}(拒保留哨兵/超长租户名,与所有其它
|
|
23
|
+
* principal 入口同一道闸);scope 键必须是 `org:` 前缀、值必须是仅含可选 `write: boolean` 的 plain object。
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseOrgDirectoryStatic(json: string): Map<string, Record<string, {
|
|
26
|
+
write?: boolean;
|
|
27
|
+
}>>;
|
|
28
|
+
/**
|
|
29
|
+
* 目录条目的判别联合(设计裁定 N3/C5):`granted` 是取数成功——包括零授权空表(负结果,由 resolver
|
|
30
|
+
* 判终局拒);`unavailable` 是瞬时臂(取数本身失败/形状不可信/LB 陈旧副本),只在这一支才该让 core 重试。
|
|
31
|
+
*/
|
|
32
|
+
export type OrgDirectoryLookup = {
|
|
33
|
+
kind: "granted";
|
|
34
|
+
scopes: Readonly<Record<string, {
|
|
35
|
+
write?: boolean;
|
|
36
|
+
}>>;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "unavailable";
|
|
39
|
+
reason: OrgDirectoryUnavailableReason;
|
|
40
|
+
retryAfterMs: number;
|
|
41
|
+
};
|
|
42
|
+
/** {@link createOrgMemoryDirectory} 的产物 —— 单一 per-principal 查表口。 */
|
|
43
|
+
export interface OrgMemoryDirectory {
|
|
44
|
+
lookup(principal: string): Promise<OrgDirectoryLookup>;
|
|
45
|
+
}
|
|
46
|
+
export interface OrgMemoryDirectoryOptions {
|
|
47
|
+
/** 远程腿:返回 caps 响应的 `orgMemory` 段原值(unknown,本模块用 `PrincipalOrgMemoryWire.safeParse` 校验)。
|
|
48
|
+
* 返回 undefined = 响应里键整体缺席(旧 center 能力握手)⇒ unavailable("section_absent")。
|
|
49
|
+
* throw = fetch 失败 ⇒ unavailable("fetch_failed")。
|
|
50
|
+
* 与 `staticTable` 互斥:二者恰好给一个(装配点保证;都给或都缺 = 构造期 throw,fail-loud)。 */
|
|
51
|
+
fetchSection?: (principal: string) => Promise<unknown>;
|
|
52
|
+
/** 单机腿:{@link parseOrgDirectoryStatic} 的产物。查表恒「取数成功」:缺 principal ⇒ granted 空表。 */
|
|
53
|
+
staticTable?: Map<string, Record<string, {
|
|
54
|
+
write?: boolean;
|
|
55
|
+
}>>;
|
|
56
|
+
/** granted(含负结果)缓存 TTL——ms。负结果与正授权同 TTL:未授权 principal 不该比授权 principal 打出
|
|
57
|
+
* 更多流量。默认值不在本模块(接线点决定,设计定 60_000)。 */
|
|
58
|
+
grantTtlMs: number;
|
|
59
|
+
/** 一次 unavailable 之后,同 principal 在这个窗口内的 lookup 直接返回 unavailable(不重新 fetch)——
|
|
60
|
+
* 防止对一个持续故障/未授权目标反复打 center。默认值不在本模块(接线点决定,设计定 10_000)。 */
|
|
61
|
+
unavailableBackoffMs: number;
|
|
62
|
+
/** 时间注入(测试用);默认 `Date.now`。 */
|
|
63
|
+
now?: () => number;
|
|
64
|
+
}
|
|
65
|
+
type OrgDirectoryUnavailableReason = "fetch_failed" | "section_absent" | "malformed" | "stale_generation";
|
|
66
|
+
/**
|
|
67
|
+
* 建目录:远程腿(fetchSection,per-principal TTL 缓存 + in-flight 去重 + 退避窗 + gen 高水位)或单机腿
|
|
68
|
+
* (staticTable,恒同步成功、无 TTL/退避语义——operator 自证配置面)。两腿互斥,装配点必须恰好给一个。
|
|
69
|
+
*/
|
|
70
|
+
export declare function createOrgMemoryDirectory(opts: OrgMemoryDirectoryOptions): OrgMemoryDirectory;
|
|
71
|
+
export interface MemoryScopeAdmissionOptions {
|
|
72
|
+
/** "audit": 判决照算但从不真拒——会拒的结果(终局 ok:false 或瞬时 throw)一律换成全额放行 + 观测记录
|
|
73
|
+
* would-deny;真通过的判决(含 write 收窄为 null)照常返回。"enforce": 判决即结果。 */
|
|
74
|
+
mode: "audit" | "enforce";
|
|
75
|
+
/** 观测 hook。outcome ∈ "ok" | "denied" | "principal_missing" | "directory_absent" | "directory_stale" |
|
|
76
|
+
* "directory_malformed"。audit 模式下的 would-deny 也走这里,`details.audited === true`。hook 本身绝不
|
|
77
|
+
* 允许打断准入判决——一律吞掉它可能抛出的异常。 */
|
|
78
|
+
onOutcome?: (outcome: string, details?: Record<string, unknown>) => void;
|
|
79
|
+
}
|
|
80
|
+
/** core 侧「瞬时不可用,重试」契约的载体:一个带 `retryAfterMs`(ms)属性的真 Error 子类,core 据此铸
|
|
81
|
+
* memory.admission_required 终局码并透传该值。子类而非事后挂属性——避免任何宽松断言就能拿到正确类型。 */
|
|
82
|
+
export declare class OrgMemoryAdmissionRetryError extends Error {
|
|
83
|
+
readonly retryAfterMs: number;
|
|
84
|
+
constructor(message: string, retryAfterMs: number);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `RunnerDeps.memoryScopeAdmission` 的 server 实装。deployment-origin 的 requested 项一律放行(operator
|
|
88
|
+
* 自证——v1 不做中央收窄部署声明);request-origin 项查 `directory.lookup(principal)`:granted 时逐项核
|
|
89
|
+
* 对是否在授权集里(任何一项缺席 = 整体终局拒绝,不做部分放行——reason 只点名调用方自己请求过的 scope
|
|
90
|
+
* 串,不逐 scope 展开「为什么」,避免变成成员探针);unavailable 时 throw 一个带 `retryAfterMs` 的
|
|
91
|
+
* {@link OrgMemoryAdmissionRetryError}。write 面独立收窄:deployment-origin 恒授,request-origin 仅当目录
|
|
92
|
+
* 条目 `write === true` 才授,否则收窄为 null(即使读侧整体放行)。
|
|
93
|
+
*/
|
|
94
|
+
export declare function createMemoryScopeAdmission(directory: OrgMemoryDirectory, opts: MemoryScopeAdmissionOptions): MemoryScopeAdmission;
|
|
95
|
+
export {};
|
|
96
|
+
//# sourceMappingURL=org-memory-admission.d.ts.map
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { PrincipalOrgMemoryWire } from "@sema-agent/registry-core";
|
|
2
|
+
import { assertPrincipalShape } from "./security.js";
|
|
3
|
+
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
4
|
+
const ORG_SCOPE_RE = /^org:\S+$/;
|
|
5
|
+
/**
|
|
6
|
+
* 单机腿:装载 `MEMORY_ORG_DIRECTORY_JSON`(`Record<principal, Record<orgScope, {write?}>>`)。启动期调用,
|
|
7
|
+
* **fail-loud**——任何非法直接 throw 且消息点名坏在哪个键(响亮拒是正确方向:一张半坏的授权表比拒绝
|
|
8
|
+
* 启动更危险)。principal 键复用 {@link assertPrincipalShape}(拒保留哨兵/超长租户名,与所有其它
|
|
9
|
+
* principal 入口同一道闸);scope 键必须是 `org:` 前缀、值必须是仅含可选 `write: boolean` 的 plain object。
|
|
10
|
+
*/
|
|
11
|
+
export function parseOrgDirectoryStatic(json) {
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(json);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON is not valid JSON: ${e.message}`);
|
|
18
|
+
}
|
|
19
|
+
if (!isPlainObject(parsed)) {
|
|
20
|
+
throw new Error("MEMORY_ORG_DIRECTORY_JSON must be a JSON object: Record<principal, Record<orgScope, {write?: boolean}>>");
|
|
21
|
+
}
|
|
22
|
+
const table = new Map();
|
|
23
|
+
for (const [principal, scopesRaw] of Object.entries(parsed)) {
|
|
24
|
+
assertPrincipalShape(principal); // reserved sentinel / oversized tenant name → throws (same gate as every other principal entry point)
|
|
25
|
+
if (!isPlainObject(scopesRaw)) {
|
|
26
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}] must be an object of org scopes`);
|
|
27
|
+
}
|
|
28
|
+
const scopes = {};
|
|
29
|
+
for (const [scope, entryRaw] of Object.entries(scopesRaw)) {
|
|
30
|
+
if (!ORG_SCOPE_RE.test(scope)) {
|
|
31
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}] has an invalid org scope key ${JSON.stringify(scope)} (must match /^org:\\S+$/)`);
|
|
32
|
+
}
|
|
33
|
+
if (!isPlainObject(entryRaw)) {
|
|
34
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}][${JSON.stringify(scope)}] must be an object`);
|
|
35
|
+
}
|
|
36
|
+
for (const key of Object.keys(entryRaw)) {
|
|
37
|
+
if (key !== "write") {
|
|
38
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}][${JSON.stringify(scope)}] has an unexpected key ${JSON.stringify(key)} (only "write" is allowed)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (entryRaw.write !== undefined && typeof entryRaw.write !== "boolean") {
|
|
42
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}][${JSON.stringify(scope)}].write must be a boolean`);
|
|
43
|
+
}
|
|
44
|
+
scopes[scope] = entryRaw.write !== undefined ? { write: entryRaw.write } : {};
|
|
45
|
+
}
|
|
46
|
+
table.set(principal, scopes);
|
|
47
|
+
}
|
|
48
|
+
return table;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 建目录:远程腿(fetchSection,per-principal TTL 缓存 + in-flight 去重 + 退避窗 + gen 高水位)或单机腿
|
|
52
|
+
* (staticTable,恒同步成功、无 TTL/退避语义——operator 自证配置面)。两腿互斥,装配点必须恰好给一个。
|
|
53
|
+
*/
|
|
54
|
+
export function createOrgMemoryDirectory(opts) {
|
|
55
|
+
if ((opts.fetchSection !== undefined) === (opts.staticTable !== undefined)) {
|
|
56
|
+
throw new Error("createOrgMemoryDirectory requires exactly one of fetchSection or staticTable (both given or both missing)");
|
|
57
|
+
}
|
|
58
|
+
if (opts.staticTable !== undefined) {
|
|
59
|
+
const table = opts.staticTable;
|
|
60
|
+
return {
|
|
61
|
+
async lookup(principal) {
|
|
62
|
+
return { kind: "granted", scopes: table.get(principal) ?? {} };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (opts.fetchSection === undefined) {
|
|
67
|
+
// Unreachable given the xor guard above (staticTable was undefined, so fetchSection must not be) — this
|
|
68
|
+
// check exists ONLY so the type checker narrows fetchSection to a function without a cast.
|
|
69
|
+
throw new Error("createOrgMemoryDirectory: fetchSection missing (invariant violated)");
|
|
70
|
+
}
|
|
71
|
+
const fetchSection = opts.fetchSection;
|
|
72
|
+
const grantTtlMs = opts.grantTtlMs;
|
|
73
|
+
const backoffMs = opts.unavailableBackoffMs;
|
|
74
|
+
const now = opts.now ?? (() => Date.now());
|
|
75
|
+
const states = new Map();
|
|
76
|
+
const inflight = new Map();
|
|
77
|
+
function markUnavailable(state, reason) {
|
|
78
|
+
state.unavailable = { until: now() + backoffMs, reason };
|
|
79
|
+
// C6: retryAfterMs never 0/negative — the window just opened, so its remaining time IS backoffMs.
|
|
80
|
+
return { kind: "unavailable", reason, retryAfterMs: Math.max(backoffMs, 1000) };
|
|
81
|
+
}
|
|
82
|
+
async function doFetch(principal, state) {
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = await fetchSection(principal);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return markUnavailable(state, "fetch_failed");
|
|
89
|
+
}
|
|
90
|
+
if (raw === undefined)
|
|
91
|
+
return markUnavailable(state, "section_absent");
|
|
92
|
+
const parsed = PrincipalOrgMemoryWire.safeParse(raw);
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
// malformed: does NOT touch state.grant — a bad shape must not refresh (or clear) an existing granted entry's
|
|
95
|
+
// timestamp; a still-fresh old grant keeps serving from cache on the NEXT lookup (checked before we ever get here).
|
|
96
|
+
return markUnavailable(state, "malformed");
|
|
97
|
+
}
|
|
98
|
+
if (state.highWaterGen !== undefined && parsed.data.gen < state.highWaterGen) {
|
|
99
|
+
// LB stale-replica defense: an older generation than one we've already seen is NEVER adopted.
|
|
100
|
+
return markUnavailable(state, "stale_generation");
|
|
101
|
+
}
|
|
102
|
+
state.highWaterGen = parsed.data.gen;
|
|
103
|
+
state.grant = { scopes: parsed.data.scopes, at: now() };
|
|
104
|
+
delete state.unavailable; // a real success clears any prior backoff window
|
|
105
|
+
return { kind: "granted", scopes: parsed.data.scopes };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
async lookup(principal) {
|
|
109
|
+
let state = states.get(principal);
|
|
110
|
+
if (!state) {
|
|
111
|
+
state = {};
|
|
112
|
+
states.set(principal, state);
|
|
113
|
+
}
|
|
114
|
+
const t = now();
|
|
115
|
+
// 1. Fresh grant (positive OR negative/empty result) always wins — no fetch, no backoff check.
|
|
116
|
+
if (state.grant && state.grant.at + grantTtlMs > t) {
|
|
117
|
+
return { kind: "granted", scopes: state.grant.scopes };
|
|
118
|
+
}
|
|
119
|
+
// 2. Inside an open backoff window → answer from the window WITHOUT re-fetching.
|
|
120
|
+
if (state.unavailable && state.unavailable.until > t) {
|
|
121
|
+
return { kind: "unavailable", reason: state.unavailable.reason, retryAfterMs: Math.max(state.unavailable.until - t, 1000) };
|
|
122
|
+
}
|
|
123
|
+
// 3. Grant expired (or never existed) and no active backoff → fetch, deduping concurrent callers.
|
|
124
|
+
const dup = inflight.get(principal);
|
|
125
|
+
if (dup)
|
|
126
|
+
return dup;
|
|
127
|
+
const p = doFetch(principal, state).finally(() => inflight.delete(principal));
|
|
128
|
+
inflight.set(principal, p);
|
|
129
|
+
return p;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** core 侧「瞬时不可用,重试」契约的载体:一个带 `retryAfterMs`(ms)属性的真 Error 子类,core 据此铸
|
|
134
|
+
* memory.admission_required 终局码并透传该值。子类而非事后挂属性——避免任何宽松断言就能拿到正确类型。 */
|
|
135
|
+
export class OrgMemoryAdmissionRetryError extends Error {
|
|
136
|
+
retryAfterMs;
|
|
137
|
+
constructor(message, retryAfterMs) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.retryAfterMs = retryAfterMs;
|
|
140
|
+
this.name = "OrgMemoryAdmissionRetryError";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `RunnerDeps.memoryScopeAdmission` 的 server 实装。deployment-origin 的 requested 项一律放行(operator
|
|
145
|
+
* 自证——v1 不做中央收窄部署声明);request-origin 项查 `directory.lookup(principal)`:granted 时逐项核
|
|
146
|
+
* 对是否在授权集里(任何一项缺席 = 整体终局拒绝,不做部分放行——reason 只点名调用方自己请求过的 scope
|
|
147
|
+
* 串,不逐 scope 展开「为什么」,避免变成成员探针);unavailable 时 throw 一个带 `retryAfterMs` 的
|
|
148
|
+
* {@link OrgMemoryAdmissionRetryError}。write 面独立收窄:deployment-origin 恒授,request-origin 仅当目录
|
|
149
|
+
* 条目 `write === true` 才授,否则收窄为 null(即使读侧整体放行)。
|
|
150
|
+
*/
|
|
151
|
+
export function createMemoryScopeAdmission(directory, opts) {
|
|
152
|
+
const emit = (outcome, details) => {
|
|
153
|
+
try {
|
|
154
|
+
opts.onOutcome?.(outcome, details);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* an observability hook must never break admission */
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return async ({ principal, requested, requestedWriteScope }) => {
|
|
161
|
+
const requestOriginRequested = requested.filter((r) => r.origin === "request");
|
|
162
|
+
const writeIsRequestOrigin = requestedWriteScope?.origin === "request";
|
|
163
|
+
const hasRequestOrigin = requestOriginRequested.length > 0 || writeIsRequestOrigin;
|
|
164
|
+
const fullScopes = requested.map((r) => r.scope);
|
|
165
|
+
const fullWriteScope = requestedWriteScope?.scope ?? null;
|
|
166
|
+
const admitFull = () => ({ ok: true, scopes: fullScopes, writeScope: fullWriteScope });
|
|
167
|
+
async function decide() {
|
|
168
|
+
// deployment-only requests (no request-origin item anywhere, including the write slot): nothing to
|
|
169
|
+
// look up — v1 does not centrally narrow operator-declared deployment scopes.
|
|
170
|
+
if (!hasRequestOrigin)
|
|
171
|
+
return { kind: "admit", verdict: admitFull() };
|
|
172
|
+
if (principal === undefined) {
|
|
173
|
+
// Defensive arm: core already refuses this shape itself when a request-origin org scope is present
|
|
174
|
+
// with no principal, but the resolver must not assume that guard always ran first.
|
|
175
|
+
return { kind: "deny", reason: "principal absent", outcome: "principal_missing" };
|
|
176
|
+
}
|
|
177
|
+
const lookup = await directory.lookup(principal);
|
|
178
|
+
if (lookup.kind === "unavailable") {
|
|
179
|
+
const outcome = lookup.reason === "stale_generation" ? "directory_stale" : lookup.reason === "malformed" ? "directory_malformed" : "directory_absent"; // fetch_failed | section_absent
|
|
180
|
+
return { kind: "unavailable", retryAfterMs: lookup.retryAfterMs, outcome, lookupReason: lookup.reason, details: { reason: lookup.reason } };
|
|
181
|
+
}
|
|
182
|
+
const deniedScopes = requestOriginRequested.filter((r) => !(r.scope in lookup.scopes)).map((r) => r.scope);
|
|
183
|
+
if (deniedScopes.length > 0) {
|
|
184
|
+
return {
|
|
185
|
+
kind: "deny",
|
|
186
|
+
reason: `org scope(s) not admitted for this principal: ${deniedScopes.join(", ")}`,
|
|
187
|
+
outcome: "denied",
|
|
188
|
+
details: { deniedScopes },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const writeScope = requestedWriteScope === null
|
|
192
|
+
? null
|
|
193
|
+
: requestedWriteScope.origin === "deployment"
|
|
194
|
+
? requestedWriteScope.scope
|
|
195
|
+
: lookup.scopes[requestedWriteScope.scope]?.write === true
|
|
196
|
+
? requestedWriteScope.scope
|
|
197
|
+
: null;
|
|
198
|
+
return { kind: "admit", verdict: { ok: true, scopes: fullScopes, writeScope } };
|
|
199
|
+
}
|
|
200
|
+
const decision = await decide();
|
|
201
|
+
if (decision.kind === "admit") {
|
|
202
|
+
// C10(audit=纯观察**零**行为变化):write 收窄也是行为变化(enforce 下 harvest 静默零提交)——
|
|
203
|
+
// audit 模式对「读面通过但 write 未显式授」的判决只记 would-narrow,返回未收窄的全额;
|
|
204
|
+
// enforce 照常收窄。ok:true 分支的 writeScope 恒为 fullWriteScope 或 null(decide 只做这两值)。
|
|
205
|
+
if (opts.mode === "audit" && decision.verdict.ok && decision.verdict.writeScope !== fullWriteScope) {
|
|
206
|
+
emit("ok", { audited: true, wouldNarrowWriteScope: true });
|
|
207
|
+
return admitFull();
|
|
208
|
+
}
|
|
209
|
+
emit("ok");
|
|
210
|
+
return decision.verdict;
|
|
211
|
+
}
|
|
212
|
+
if (opts.mode === "audit") {
|
|
213
|
+
// Zero behavior change: a would-deny (terminal or transient) is overridden to a full, UNNARROWED admit —
|
|
214
|
+
// the real decision is only ever recorded via onOutcome, never enforced.
|
|
215
|
+
emit(decision.outcome, { ...decision.details, audited: true, wouldDeny: true, ...(decision.kind === "unavailable" ? { retryAfterMs: decision.retryAfterMs } : { reason: decision.reason }) });
|
|
216
|
+
return admitFull();
|
|
217
|
+
}
|
|
218
|
+
emit(decision.outcome, decision.details);
|
|
219
|
+
if (decision.kind === "deny")
|
|
220
|
+
return { ok: false, reason: decision.reason };
|
|
221
|
+
throw new OrgMemoryAdmissionRetryError(`org memory directory unavailable (${decision.lookupReason})`, decision.retryAfterMs);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=org-memory-admission.js.map
|
package/dist/question.d.ts
CHANGED
|
@@ -67,6 +67,11 @@ export declare class QuestionCoordinator {
|
|
|
67
67
|
};
|
|
68
68
|
/** Test/observability hook: number of currently-parked questions. */
|
|
69
69
|
pendingCount(): number;
|
|
70
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条活流腿的
|
|
71
|
+
* per-run 上下文里(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade=false)。
|
|
72
|
+
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
73
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。 */
|
|
74
|
+
hasLiveContext(): boolean;
|
|
70
75
|
private countersFor;
|
|
71
76
|
}
|
|
72
77
|
//# sourceMappingURL=question.d.ts.map
|
package/dist/question.js
CHANGED
|
@@ -192,6 +192,13 @@ export class QuestionCoordinator {
|
|
|
192
192
|
pendingCount() {
|
|
193
193
|
return this.pending.size;
|
|
194
194
|
}
|
|
195
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条活流腿的
|
|
196
|
+
* per-run 上下文里(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade=false)。
|
|
197
|
+
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
198
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。 */
|
|
199
|
+
hasLiveContext() {
|
|
200
|
+
return this.als.getStore() !== undefined;
|
|
201
|
+
}
|
|
195
202
|
countersFor(taskId) {
|
|
196
203
|
let rc = this.counters.get(taskId);
|
|
197
204
|
if (!rc) {
|
package/dist/runs.d.ts
CHANGED
|
@@ -106,6 +106,13 @@ export declare function isSessionConflictResult(result: {
|
|
|
106
106
|
* UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
|
|
107
107
|
* point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
|
|
108
108
|
* billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
|
|
109
|
+
*
|
|
110
|
+
* design/170 件A(#148 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
|
|
111
|
+
* (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
|
|
112
|
+
* plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
|
|
113
|
+
* the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
|
|
114
|
+
* result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
|
|
115
|
+
* codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
|
|
109
116
|
*/
|
|
110
117
|
export declare function resumeAtHttpStatus(result: {
|
|
111
118
|
status: string;
|
package/dist/runs.js
CHANGED
|
@@ -3,7 +3,7 @@ import { withPrincipal } from "./observability/principal-context.js";
|
|
|
3
3
|
import { redactSecrets } from "./trace/redact.js";
|
|
4
4
|
import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "./trace/project.js";
|
|
5
5
|
import { createLedgerSink } from "./trace/ledger-sink.js";
|
|
6
|
-
import { fleetRunResiduals } from "./fleet/fleet-bus.js";
|
|
6
|
+
import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
|
|
7
7
|
import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
|
|
8
8
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
|
|
9
9
|
/**
|
|
@@ -148,6 +148,13 @@ export function isSessionConflictResult(result) {
|
|
|
148
148
|
* UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
|
|
149
149
|
* point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
|
|
150
150
|
* billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
|
|
151
|
+
*
|
|
152
|
+
* design/170 件A(#148 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
|
|
153
|
+
* (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
|
|
154
|
+
* plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
|
|
155
|
+
* the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
|
|
156
|
+
* result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
|
|
157
|
+
* codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
|
|
151
158
|
*/
|
|
152
159
|
export function resumeAtHttpStatus(result) {
|
|
153
160
|
if (result.status !== "failed")
|
|
@@ -156,6 +163,10 @@ export function resumeAtHttpStatus(result) {
|
|
|
156
163
|
return 404;
|
|
157
164
|
if (result.errorCode === "rewind_snapshot.unresolvable")
|
|
158
165
|
return 422;
|
|
166
|
+
if (result.errorCode === "memory.admission_denied")
|
|
167
|
+
return 403;
|
|
168
|
+
if (result.errorCode === "memory.admission_required")
|
|
169
|
+
return 503;
|
|
159
170
|
if (!(result.errorCode?.startsWith("resume_at.") ?? false))
|
|
160
171
|
return undefined;
|
|
161
172
|
return result.errorCode === "resume_at.not_found" ? 404 : 422;
|
|
@@ -575,8 +586,11 @@ promptManifests) {
|
|
|
575
586
|
}
|
|
576
587
|
// id-domain alias (core): fleet child rows key by the TICK's taskId = the child's sessionId
|
|
577
588
|
// (uuid domain); the notification's task_id is the a* domain — payload.sessionId IS the tick-domain
|
|
578
|
-
// alias, so flip by it (fallback task_id for pre-1.238 payloads
|
|
579
|
-
|
|
589
|
+
// alias, so flip by it (fallback task_id for pre-1.238 payloads).
|
|
590
|
+
// [2687-cli] 幽灵行案:onChildTerminal 只对 **agent 族终态**调用(isFleetAgentTerminalNotification
|
|
591
|
+
// 单源判别,理由与病灶链见其 doc 注)。通知帧/park 面照走,只掐 fleet 铸行。三消费点孪生同形
|
|
592
|
+
// (http/server.ts resume 腿、routes/tasks.ts sync 腿)。
|
|
593
|
+
const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPublisher?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
|
|
580
594
|
const parked = !legLive && Boolean(workflowCompletionInbox && spec.sessionId);
|
|
581
595
|
// diagnosability: one line per observed bg completion. hadFleetRow=false = the terminal frame was
|
|
582
596
|
// FLIP-THROUGH synthesized (the row was gone — parent settle removed it, or a bg BASH never ticked).
|
|
@@ -86,15 +86,23 @@ export declare function compileCommandPolicy(rules: CommandRule[] | undefined):
|
|
|
86
86
|
*/
|
|
87
87
|
export declare function autonomyOverrides(autonomy: Autonomy | undefined): Partial<TaskSpec>;
|
|
88
88
|
/**
|
|
89
|
-
* Apply the operator's runtime governance (autonomy + commandPolicy) onto a base
|
|
90
|
-
* SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
91
|
-
* baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
|
|
92
|
-
* (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
|
|
93
|
-
* field — surfacing a misconfiguration loudly instead of silently
|
|
94
|
-
* when there is nothing to apply
|
|
89
|
+
* Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate) onto a base
|
|
90
|
+
* `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
91
|
+
* override (combined onto the baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
|
|
92
|
+
* `handsReadOnly` / `shellGate` overrides (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
|
|
93
|
+
* override would LOOSEN a stricter base safety field — surfacing a misconfiguration loudly instead of silently
|
|
94
|
+
* weakening the gate. Returns `base` untouched when there is nothing to apply.
|
|
95
|
+
*
|
|
96
|
+
* #153([2713] server 分单②):`MANUAL_MODE_SHELL_GATE` 从 settings 折叠搬到这里**无条件施加**。旧家
|
|
97
|
+
* (task-settings deriveSettingsPolicy 的 default/auto/acceptEdits 三臂)让一个**部署级 tighten-only 旋钮
|
|
98
|
+
* 的生死由客户端表态在场性决定**:body 无 settings/permissionMode(cli print/headless 腿按设计不 stamp)
|
|
99
|
+
* ⇒ 旋钮静默失效(clay 实机 Monitor 案「门根本没铸」);bypassPermissions 一句话掀掉部署闸;非 host lane
|
|
100
|
+
* fsWriteGate wiring 不建 ⇒ 沙箱 lane 全失效。governance 层与 autonomy 同拍施加后三者全闭。rank 合成:
|
|
101
|
+
* autonomy 派生值与旋钮取大(ask→"always" 压过 "classify"),再对 base 只升不降(防 tightenTaskSpec throw)。
|
|
95
102
|
*/
|
|
96
103
|
export declare function applyRuntimeGovernance(base: TaskSpec, governance: {
|
|
97
104
|
autonomy?: Autonomy;
|
|
98
105
|
commandPolicy?: CommandRule[];
|
|
106
|
+
manualModeShellGate?: "always" | "classify";
|
|
99
107
|
}): TaskSpec;
|
|
100
108
|
//# sourceMappingURL=runtime-governance.d.ts.map
|
|
@@ -204,19 +204,40 @@ export function autonomyOverrides(autonomy) {
|
|
|
204
204
|
return {};
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
|
+
/** core `tighten-task-spec` 的 SHELL_GATE_RANK 镜像(单一属主:#153 搬家后本文件是 server 侧唯一副本;
|
|
208
|
+
* task-settings 的旧副本随 settings 折叠臂一并拆除)。tightenTaskSpec 的契约是「override 自身必须收紧」
|
|
209
|
+
* 而非「取两者较严」——低于 base 的 override 直接 throw,所以施加前要按 rank 只升不降。 */
|
|
210
|
+
const SHELL_GATE_RANK = { off: 0, classify: 1, always: 2 };
|
|
207
211
|
/**
|
|
208
|
-
* Apply the operator's runtime governance (autonomy + commandPolicy) onto a base
|
|
209
|
-
* SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
210
|
-
* baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
|
|
211
|
-
* (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
|
|
212
|
-
* field — surfacing a misconfiguration loudly instead of silently
|
|
213
|
-
* when there is nothing to apply
|
|
212
|
+
* Apply the operator's runtime governance (autonomy + commandPolicy + manualModeShellGate) onto a base
|
|
213
|
+
* `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link tightenTaskSpec} call: commandPolicy compiles to a `toolPolicy`
|
|
214
|
+
* override (combined onto the baseline by `tightenTaskSpec` — TRAP #1), and autonomy expands to
|
|
215
|
+
* `handsReadOnly` / `shellGate` overrides (TRAP #2). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any
|
|
216
|
+
* override would LOOSEN a stricter base safety field — surfacing a misconfiguration loudly instead of silently
|
|
217
|
+
* weakening the gate. Returns `base` untouched when there is nothing to apply.
|
|
218
|
+
*
|
|
219
|
+
* #153([2713] server 分单②):`MANUAL_MODE_SHELL_GATE` 从 settings 折叠搬到这里**无条件施加**。旧家
|
|
220
|
+
* (task-settings deriveSettingsPolicy 的 default/auto/acceptEdits 三臂)让一个**部署级 tighten-only 旋钮
|
|
221
|
+
* 的生死由客户端表态在场性决定**:body 无 settings/permissionMode(cli print/headless 腿按设计不 stamp)
|
|
222
|
+
* ⇒ 旋钮静默失效(clay 实机 Monitor 案「门根本没铸」);bypassPermissions 一句话掀掉部署闸;非 host lane
|
|
223
|
+
* fsWriteGate wiring 不建 ⇒ 沙箱 lane 全失效。governance 层与 autonomy 同拍施加后三者全闭。rank 合成:
|
|
224
|
+
* autonomy 派生值与旋钮取大(ask→"always" 压过 "classify"),再对 base 只升不降(防 tightenTaskSpec throw)。
|
|
214
225
|
*/
|
|
215
226
|
export function applyRuntimeGovernance(base, governance) {
|
|
216
227
|
const overrides = { ...autonomyOverrides(governance.autonomy) };
|
|
217
228
|
const coarse = compileCommandPolicy(governance.commandPolicy);
|
|
218
229
|
if (coarse)
|
|
219
230
|
overrides.toolPolicy = coarse;
|
|
231
|
+
if (governance.manualModeShellGate !== undefined) {
|
|
232
|
+
const candidate = overrides.shellGate !== undefined && SHELL_GATE_RANK[overrides.shellGate] >= SHELL_GATE_RANK[governance.manualModeShellGate]
|
|
233
|
+
? overrides.shellGate
|
|
234
|
+
: governance.manualModeShellGate;
|
|
235
|
+
// base 已更严(如 resume 重放时 base 带 always)⇒ 省略,让 base 原样保留(省略=行为等价,不 throw)。
|
|
236
|
+
if (SHELL_GATE_RANK[candidate] >= SHELL_GATE_RANK[base.shellGate ?? "off"])
|
|
237
|
+
overrides.shellGate = candidate;
|
|
238
|
+
else
|
|
239
|
+
delete overrides.shellGate;
|
|
240
|
+
}
|
|
220
241
|
if (overrides.toolPolicy === undefined && overrides.handsReadOnly === undefined && overrides.shellGate === undefined) {
|
|
221
242
|
return base; // nothing to govern → avoid a needless tightenTaskSpec pass
|
|
222
243
|
}
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -181,19 +181,21 @@ export interface FsWriteGateWiring {
|
|
|
181
181
|
* now closed). Source = config `SENSITIVE_WRITE_PATTERNS` (unset ⇒ core RECOMMENDED_SENSITIVE_PATTERNS; the
|
|
182
182
|
* set curation is core's — the server passes through). Absent/empty ⇒ no sensitive policy (pre-1.295 shape). */
|
|
183
183
|
sensitivePatterns?: string[];
|
|
184
|
-
/** [1557]§四 opt-in(cli[1555]②「echo -n … > file 绕写门」缺口的部署侧补丁,core[1556] 建议):
|
|
185
|
-
* CC manual 系模式(default/auto/acceptEdits — 见 {@link deriveSettingsPolicy})額外把
|
|
186
|
-
* `TaskSpec.shellGate` 收紧到这个值,让 Bash 走同一 fs-write 审批链(core `bashReversibilityProbe`
|
|
187
|
-
* 在 `"classify"` 下只拦构造性不可逆命令——含 shell 重定向 `>`/`>>` 等,纯读命令白名单自动放行)。
|
|
188
|
-
* **Absent(缺省)= 零行为变化**(config `MANUAL_MODE_SHELL_GATE` 未设置时不传本字段,pre-此批行为
|
|
189
|
-
* byte-for-byte;这是个 tighten-only opt-in,不是默认开启——运营方需要显式选择开启)。 */
|
|
190
|
-
shellGate?: "always" | "classify";
|
|
191
184
|
}
|
|
192
185
|
export declare function deriveSettingsPolicy(settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): {
|
|
193
186
|
toolPolicy?: ToolPolicy;
|
|
194
187
|
handsReadOnly?: boolean;
|
|
195
188
|
enablePlanMode?: boolean;
|
|
196
|
-
shellGate?: "always" | "classify";
|
|
197
189
|
};
|
|
190
|
+
/**
|
|
191
|
+
* Apply a client's parsed per-request settings onto a base `TaskSpec`, TIGHTEN-ONLY, in a SINGLE {@link
|
|
192
|
+
* tightenTaskSpec} pass (so the permission override composes deny-wins onto the baseline — TRAP #1 — and
|
|
193
|
+
* `handsReadOnly`/`onAsk` can only narrow). `tightenTaskSpec` THROWS `TaskSpecTightenError` if any override would
|
|
194
|
+
* LOOSEN a stricter base — surfaced by the caller as a 4xx (a client settings stamp that tries to widen is a
|
|
195
|
+
* client error, never a silent weakening). `model`/`outputStyle` are applied directly (model is the caller's
|
|
196
|
+
* choice WITHIN the catalog — the caller gates it before calling; outputStyle appends to the system prompt).
|
|
197
|
+
*
|
|
198
|
+
* Returns `base` untouched when the parsed settings project nothing onto the spec.
|
|
199
|
+
*/
|
|
198
200
|
export declare function applyTaskSettings(base: TaskSpec, settings: ParsedTaskSettings, gate?: FsWriteGateWiring, workflowGate?: WorkflowGateWiring): TaskSpec;
|
|
199
201
|
//# sourceMappingURL=task-settings.d.ts.map
|