@sema-agent/server 7.116.1 → 7.118.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/MIGRATION.md +195 -0
- package/USAGE.md +65 -41
- package/dist/approval-card.d.ts +82 -18
- package/dist/approval-card.js +46 -4
- package/dist/approval.d.ts +4 -12
- package/dist/approval.js +4 -26
- package/dist/boot/budget-tracing.js +2 -3
- package/dist/boot/config-center.d.ts +2 -2
- package/dist/boot/config-center.js +5 -7
- package/dist/boot/parked-revive-gate.d.ts +1 -7
- package/dist/boot/parked-revive-gate.js +2 -12
- package/dist/boot/resolve-spec.js +18 -16
- package/dist/boot/runner-deps.d.ts +5 -3
- package/dist/boot/runner-deps.js +6 -4
- package/dist/boot/stage-07-capability-layer.js +2 -2
- package/dist/brain.d.ts +17 -8
- package/dist/brain.js +10 -7
- package/dist/capabilities/hands-lane.d.ts +25 -0
- package/dist/capabilities/hands-lane.js +1 -1
- package/dist/capabilities/mcp-probe.d.ts +2 -2
- package/dist/config-catalog.js +3 -2
- package/dist/config-center/apply-effective.d.ts +6 -16
- package/dist/config-center/apply-effective.js +0 -6
- package/dist/config-center/facade.d.ts +1 -1
- package/dist/config-center/facade.js +1 -1
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-types.d.ts +12 -16
- package/dist/config.d.ts +32 -29
- package/dist/config.js +20 -27
- package/dist/deployment-governance.d.ts +13 -19
- package/dist/deployment-governance.js +7 -14
- package/dist/governance-overrides.d.ts +20 -0
- package/dist/governance-overrides.js +8 -0
- package/dist/host-decision.d.ts +83 -25
- package/dist/host-decision.js +31 -1
- package/dist/http/admission.js +13 -7
- package/dist/http/approval-sweeps.js +16 -13
- package/dist/http/checkpoint-ctx.d.ts +83 -24
- package/dist/http/checkpoint-ctx.js +68 -8
- package/dist/http/mcp-dial-deadline.d.ts +35 -0
- package/dist/http/mcp-dial-deadline.js +21 -0
- package/dist/http/memory-compliance-failure-reply.d.ts +11 -5
- package/dist/http/memory-compliance-failure-reply.js +17 -8
- package/dist/http/memory-store-refusal-reply.d.ts +14 -11
- package/dist/http/memory-store-refusal-reply.js +16 -5
- package/dist/http/resume-legs.js +18 -18
- package/dist/http/retired-skill-manifest.d.ts +1 -1
- package/dist/http/route-ctx.d.ts +3 -3
- package/dist/http/routes/approvals-assistant.js +21 -24
- package/dist/http/routes/capabilities.js +5 -3
- package/dist/http/routes/mcp-reconnect.d.ts +17 -0
- package/dist/http/routes/mcp-reconnect.js +6 -1
- package/dist/http/routes/memory-bundle.js +1 -1
- package/dist/http/routes/memory-compliance.js +1 -1
- package/dist/http/routes/memory-origin.js +1 -1
- package/dist/http/routes/runs.js +53 -33
- package/dist/http/routes/sessions.d.ts +1 -1
- package/dist/http/routes/sessions.js +3 -6
- package/dist/http/routes/workflows.js +2 -0
- package/dist/http/wire-types.d.ts +6 -1
- package/dist/memory-store-refusal.d.ts +42 -17
- package/dist/memory-store-refusal.js +25 -6
- package/dist/model-select.d.ts +16 -5
- package/dist/model-select.js +7 -3
- package/dist/model-unreachable-notice.d.ts +1 -1
- package/dist/namespaced-rule-name.d.ts +45 -39
- package/dist/namespaced-rule-name.js +36 -22
- package/dist/observability/corrupt-read-seat.d.ts +4 -0
- package/dist/observability/corrupt-read-seat.js +1 -0
- package/dist/observability/fail-open.d.ts +15 -3
- package/dist/observability/fail-open.js +15 -3
- package/dist/observability/permission-rule-events.d.ts +3 -0
- package/dist/observability/permission-rule-events.js +4 -0
- package/dist/observability/run-terminal.js +21 -1
- package/dist/operator-ask.d.ts +4 -1
- package/dist/orchestration/workflow-agent-session-index.d.ts +82 -59
- package/dist/orchestration/workflow-agent-session-index.js +80 -14
- package/dist/orchestration/workflow-completion-inbox.d.ts +5 -0
- package/dist/orchestration/workflow-completion-inbox.js +39 -17
- package/dist/orchestration/workflow-notify-journal.d.ts +1 -1
- package/dist/orchestration/workflow-notify-journal.js +7 -2
- package/dist/outbound-dispatcher.d.ts +47 -20
- package/dist/outbound-dispatcher.js +198 -23
- package/dist/parked-decide.d.ts +43 -48
- package/dist/parked-decide.js +54 -16
- package/dist/permission-rule-list.d.ts +20 -31
- package/dist/permission-rule-vocab.d.ts +4 -3
- package/dist/permission-rule-vocab.js +1 -1
- package/dist/plugins/approval-ask-store-sql.d.ts +17 -20
- package/dist/plugins/approval-ask-store-sql.js +0 -3
- package/dist/plugins/one-time-migrations.d.ts +25 -0
- package/dist/plugins/one-time-migrations.js +192 -6
- package/dist/plugins/pg-pool.js +1 -1
- package/dist/plugins/tidb-pool.js +1 -1
- package/dist/plugins/web-search.d.ts +20 -1
- package/dist/plugins/web-search.js +39 -10
- package/dist/plugins/workflow-run-store-sql.d.ts +7 -6
- package/dist/plugins/workflow-run-store-sql.js +5 -7
- package/dist/request-key-closure.js +1 -1
- package/dist/request-value-closure.d.ts +1 -1
- package/dist/router/route-orchestration.d.ts +2 -23
- package/dist/rule-entry-list.d.ts +88 -0
- package/dist/rule-entry-list.js +102 -0
- package/dist/rules-consent.js +3 -0
- package/dist/run-local.d.ts +4 -2
- package/dist/run-local.js +4 -3
- package/dist/runs.d.ts +1 -1
- package/dist/runs.js +1 -0
- package/dist/runtime-governance.d.ts +5 -21
- package/dist/runtime-governance.js +4 -8
- package/dist/session-policy-wire.d.ts +13 -12
- package/dist/session-policy-wire.js +11 -6
- package/dist/spec-fields.d.ts +13 -24
- package/dist/spec-fields.js +13 -17
- package/dist/standing-answer-grant.d.ts +14 -2
- package/dist/standing-answer-grant.js +8 -1
- package/dist/task-settings.d.ts +18 -18
- package/dist/task-settings.js +63 -32
- package/dist/tool-approval.d.ts +77 -145
- package/dist/tool-approval.js +80 -62
- package/dist/trace/core-keyset-guard.d.ts +6 -6
- package/dist/trace/engine-notice-wire.d.ts +4 -6
- package/dist/trace/engine-notice-wire.js +14 -1
- package/dist/trace/self-minted-wire.d.ts +16 -2
- package/dist/trace/self-minted-wire.js +7 -0
- package/dist/user-input-ledger.d.ts +24 -12
- package/dist/user-input-ledger.js +33 -14
- package/package.json +2 -2
- package/scripts/drain-check-local.mjs +103 -7
- package/scripts/drain-check.mysql.sql +59 -8
- package/scripts/drain-check.pg.sql +66 -10
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName,
|
|
1
|
+
import { combinePolicies, COARSE_SHELL_TOOLS as CORE_COARSE_SHELL_TOOLS, createCoarseCommandNamePolicy, DEFAULT_SUBAGENT_TOOL_NAME, parseLeadingCommandName, wrapToolPolicy, } from "@sema-agent/core";
|
|
2
2
|
import { GovernanceConfig } from "@sema-agent/settings-schema";
|
|
3
3
|
import { currentGovernanceAskMarks } from "./governance-ask-marks.js";
|
|
4
|
+
import { tightenGoverned } from "./governance-overrides.js";
|
|
4
5
|
import { createDeploymentAuthoredPolicy, operatorAsk } from "./operator-ask.js";
|
|
5
|
-
import { createRuleListPolicy } from "./permission-rule-list.js";
|
|
6
6
|
import { heldRuleName } from "./tool-rule-names.js";
|
|
7
7
|
const COMMAND_RULE_SHAPE = GovernanceConfig.shape.commandPolicy.unwrap().element.shape;
|
|
8
8
|
const COMMAND_DECISIONS = COMMAND_RULE_SHAPE.decision.options;
|
|
@@ -101,10 +101,6 @@ export function compileCommandPolicyLayers(rules) {
|
|
|
101
101
|
}
|
|
102
102
|
return policies;
|
|
103
103
|
}
|
|
104
|
-
export const APPROVAL_GRANT_SOURCE = "APPROVAL_GRANT";
|
|
105
|
-
export function compileApprovalGrantLayers(grant) {
|
|
106
|
-
return grant !== undefined && grant.length > 0 ? [createRuleListPolicy(grant, "allow", APPROVAL_GRANT_SOURCE, undefined)] : [];
|
|
107
|
-
}
|
|
108
104
|
export function autonomyOverrides(autonomy) {
|
|
109
105
|
switch (autonomy) {
|
|
110
106
|
case "read-only":
|
|
@@ -148,7 +144,7 @@ function createGovernanceShellGateMarkPolicy(marksOf, tools = COARSE_SHELL_TOOLS
|
|
|
148
144
|
export function applyRuntimeGovernance(base, governance) {
|
|
149
145
|
const marksOf = () => governance.askMarks ?? currentGovernanceAskMarks();
|
|
150
146
|
const overrides = { ...autonomyOverrides(governance.autonomy) };
|
|
151
|
-
const layers = [...compileCommandPolicyLayers(governance.commandPolicy), ...(governance.sensitivePathLayers ?? [])
|
|
147
|
+
const layers = [...compileCommandPolicyLayers(governance.commandPolicy), ...(governance.sensitivePathLayers ?? [])];
|
|
152
148
|
const governancePolicy = layers.length > 0 ? createGovernanceAskMarkingPolicy(combinePolicies(...layers.map(createDeploymentAuthoredPolicy)), marksOf) : undefined;
|
|
153
149
|
if (governancePolicy !== undefined)
|
|
154
150
|
overrides.toolPolicy = governancePolicy;
|
|
@@ -161,6 +157,6 @@ export function applyRuntimeGovernance(base, governance) {
|
|
|
161
157
|
if (Object.keys(overrides).length === 0) {
|
|
162
158
|
return base;
|
|
163
159
|
}
|
|
164
|
-
return
|
|
160
|
+
return tightenGoverned(base, overrides);
|
|
165
161
|
}
|
|
166
162
|
//# sourceMappingURL=runtime-governance.js.map
|
|
@@ -29,12 +29,13 @@
|
|
|
29
29
|
* 里同点同调它;准备阶段的名审计与本仓配置表读的是同一只 `judgeRuleToolName`)—— 本仓手写的那一只(`legacyToolNameOf`)删除,零双读;
|
|
30
30
|
* 「哪几桶装工具名」归 core(它的 `HOLDS_TOOL_NAMES`)。
|
|
31
31
|
*
|
|
32
|
-
* ── 7.116.0:写口读者再多判一问 ——
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* (
|
|
36
|
-
*
|
|
37
|
-
*
|
|
32
|
+
* ── 7.116.0:写口读者再多判一问 —— 够不着挂载工具的协议名;7.118.0 收窄到引擎读不到的那一形 ─────────────────────────
|
|
33
|
+
* 对端段铸不出 / 对端段为空(`mcp__github.com__*`、`mcp__`)自 core 7.43.0 #1355 (b) 起是引擎判官自己的判词 `unmintable_segment` ——
|
|
34
|
+
* `assertSessionRuleNamesCurrent` 拒(`legacy_tool_name`,`detail.verdict`)、本仓映射 `400 rules.legacy_tool_name`,与退役名同码同句族;本仓 7.116.0 起
|
|
35
|
+
* 那只自比对端段的对照随之退役。引擎判官放行、却一只工具都够不着的只剩**工具段为空**(`mcp__srv__`):判据与句子是 `namespaced-rule-name.ts` 那一只
|
|
36
|
+
* (配置表同调),码 `400 request.field_invalid`(会话规则写口既有的字段值码;不往 core 的判词闭集里塞本仓的词)。这一问要逐桶判,而 core 不导出
|
|
37
|
+
* `HOLDS_TOOL_NAMES` 的运行期值 ⇒ 桶表 {@link TOOL_NAME_FIELDS} 的键集从 core 判官交回的 `field` 型派生(两向 tsc 配对),随引擎判官给这一形判词那天整只退役。
|
|
38
|
+
* SQL 店写口经 {@link assertSessionRuleNamesReachable} 同判。
|
|
38
39
|
*
|
|
39
40
|
* ── 文案也在这里(codex 一轮追加:两条腿各铸一份句子 = 第二份手抄)───────────────────────────────
|
|
40
41
|
* 初版把 wire 文案留在两个路由各自铸,理由是错误文案冻结门(`test/api-error-text-freeze.test.ts`)锚的是
|
|
@@ -45,16 +46,16 @@
|
|
|
45
46
|
*/
|
|
46
47
|
import { SessionPolicyError } from "@sema-agent/core";
|
|
47
48
|
import type { SessionPermissionRules, SessionRuleNameRefusal, SessionRulesRecord } from "@sema-agent/core";
|
|
48
|
-
import {
|
|
49
|
+
import { emptyToolSegmentOf } from "./namespaced-rule-name.js";
|
|
49
50
|
/** core `SessionPermissionRules` 的桶名全集(派生,不是手抄)。 */
|
|
50
51
|
export declare const SESSION_PERMISSION_RULE_FIELDS: readonly (keyof SessionPermissionRules)[];
|
|
51
|
-
/**
|
|
52
|
+
/** 工具名桶里一条引擎判官放行、却一只工具都够不着的协议名(工具段为空);`ns` = 它的协议。 */
|
|
52
53
|
export interface SessionRuleUnreachableName {
|
|
53
54
|
readonly field: SessionRuleNameRefusal["field"];
|
|
54
55
|
readonly name: string;
|
|
55
|
-
readonly
|
|
56
|
+
readonly ns: NonNullable<ReturnType<typeof emptyToolSegmentOf>>;
|
|
56
57
|
}
|
|
57
|
-
/**
|
|
58
|
+
/** 逐桶逐条问引擎读不到的那一形;第一条交回,全部够得着 ⇒ `undefined`。调用方先过引擎判官({@link legacyToolNameRefusalOf} / `assertSessionRuleNamesCurrent`)。 */
|
|
58
59
|
export declare function unreachableSessionRuleNameOf(rules: SessionPermissionRules): SessionRuleUnreachableName | undefined;
|
|
59
60
|
/**
|
|
60
61
|
* SQL 会话规则店写口的同一问(core 判官之后、收紧门之前,与 `assertSessionRuleNamesCurrent` 同点):够不着 ⇒ 抛,一条写语句都不发。
|
|
@@ -80,7 +81,7 @@ export type SessionPermissionRulesProblem = SessionPermissionRulesShapeProblem
|
|
|
80
81
|
kind: "legacy-tool-name";
|
|
81
82
|
refusal: SessionRuleNameRefusal;
|
|
82
83
|
}
|
|
83
|
-
/** 形对、core
|
|
84
|
+
/** 形对、core 判官也放行,但工具名桶里有一条一只工具都够不着的协议名(工具段为空 —— 引擎读不到的那一形)。 */
|
|
84
85
|
| {
|
|
85
86
|
kind: "unreachable-tool-name";
|
|
86
87
|
unreachable: SessionRuleUnreachableName;
|
|
@@ -109,7 +110,7 @@ export declare function readSessionPermissionRules(raw: unknown): SessionPermiss
|
|
|
109
110
|
* 面经 {@link readSessionRulesRecords})往会话规则店写之前的**唯一**判据。两问,按序:
|
|
110
111
|
* ① 形(同 {@link readSessionPermissionRules};一份坏形的体答形的 400);
|
|
111
112
|
* ② 工具名桶里的每一条过 core 的判官({@link legacyToolNameRefusalOf})—— 写进去就让会话之后每一跑都起不来的名字,在写口拒;
|
|
112
|
-
* ③
|
|
113
|
+
* ③ 工具名桶里的每一条问引擎读不到的那一形({@link unreachableSessionRuleNameOf}:工具段为空的协议名)—— 写进去永远匹配不到任何挂载工具,在写口拒。
|
|
113
114
|
* 🔴 **只在写口,不在读回**:SQL 店读回同一行时只判形(形读者)。一条早先写进去的退役名行(或够不着的协议名行)必须**读得出来** ——
|
|
114
115
|
* 那是运营方清掉它的唯一路径(operator 的 PUT 先读写前快照),也是 core 准备阶段报 `config.legacy_tool_name`
|
|
115
116
|
* 而不是一只 500 `corrupt` 的前提。把判官放进形读者会让读回把它判坏(7.105.0 车QQ 双库套件实抓,修在同批)。
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PROTOCOL_TABLE, SessionPolicyError, assertSessionRuleNamesCurrent, judgeRuleToolName } from "@sema-agent/core";
|
|
2
|
-
import {
|
|
2
|
+
import { describeEmptyToolSegment, emptyToolSegmentOf, NO_DECLARED_TOOL_NAMES } from "./namespaced-rule-name.js";
|
|
3
3
|
const RULE_FIELDS = {
|
|
4
4
|
toolAllow: true,
|
|
5
5
|
toolDeny: true,
|
|
@@ -24,9 +24,9 @@ const TOOL_NAME_FIELDS = Object.values(TOOL_NAME_FIELD_TABLE);
|
|
|
24
24
|
export function unreachableSessionRuleNameOf(rules) {
|
|
25
25
|
for (const field of TOOL_NAME_FIELDS) {
|
|
26
26
|
for (const name of rules[field] ?? []) {
|
|
27
|
-
const
|
|
28
|
-
if (
|
|
29
|
-
return { field, name,
|
|
27
|
+
const ns = emptyToolSegmentOf(name);
|
|
28
|
+
if (ns !== undefined)
|
|
29
|
+
return { field, name, ns };
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
return undefined;
|
|
@@ -99,17 +99,22 @@ export function describeSessionPermissionRulesProblem(p) {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
function describeUnreachableSessionRuleName(u) {
|
|
102
|
-
return `${u.field} names "${u.name}", which can never match a tool (${
|
|
102
|
+
return `${u.field} names "${u.name}", which can never match a tool (${describeEmptyToolSegment(u.ns)}), so nothing was written`;
|
|
103
103
|
}
|
|
104
104
|
function describeLegacyToolName(r) {
|
|
105
105
|
switch (r.verdict) {
|
|
106
106
|
case "retired": {
|
|
107
|
-
const v = judgeRuleToolName(r.name,
|
|
107
|
+
const v = judgeRuleToolName(r.name, NO_DECLARED_TOOL_NAMES);
|
|
108
108
|
const guidance = v.kind === "retired" ? v.guidance : `renamed "${r.currentName ?? "?"}"`;
|
|
109
109
|
return `${r.field} names "${r.name}", a retired tool name (${guidance}); with that rule in place this session could not start another run, so nothing was written — use the current tool name${r.currentName !== undefined ? ` ("${r.currentName}")` : ""}`;
|
|
110
110
|
}
|
|
111
111
|
case "unprefixed_namespace":
|
|
112
112
|
return `${r.field} names "${r.name}", which contains "__" but no protocol prefix; with that rule in place this session could not start another run, so nothing was written — a protocol tool is named ${protocolToolNameShapes()}`;
|
|
113
|
+
case "unmintable_segment": {
|
|
114
|
+
const v = judgeRuleToolName(r.name, NO_DECLARED_TOOL_NAMES);
|
|
115
|
+
const guidance = v.kind === "unmintable_segment" ? v.guidance : "a protocol name whose peer segment the engine never mints";
|
|
116
|
+
return `${r.field} names "${r.name}", which can never match a tool (${guidance}), so nothing was written`;
|
|
117
|
+
}
|
|
113
118
|
default: {
|
|
114
119
|
const unhandled = r.verdict;
|
|
115
120
|
throw new Error(`unhandled legacy tool name verdict: ${JSON.stringify(unhandled)}`);
|
package/dist/spec-fields.d.ts
CHANGED
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* body WITHOUT the HTTP-layer 400 validation, so a malformed value must fall back to a safe default, never throw.
|
|
5
5
|
*/
|
|
6
6
|
import { type AgentDefinition, type TaskSpec } from "@sema-agent/core";
|
|
7
|
-
import { type UnreachableNamespacedRuleName } from "./namespaced-rule-name.js";
|
|
8
7
|
/**
|
|
9
8
|
* E12 — normalize `body.suggestNextPrompts` → `TaskSpec.suggestNextPrompts`. `true` = on with core defaults
|
|
10
9
|
* (count 3, cheap role); an object tunes `count` (clamped 1..10) / `role` (a free string — core validates it against
|
|
@@ -48,31 +47,22 @@ export declare const MAX_TASK_AGENTS = 32;
|
|
|
48
47
|
* deny 规则来源,不是围栏)的**形**谓词。两道技能门(顶层 `body.skills[]` 的 `validateUserSkills`、`agents[].skills[]` 的
|
|
49
48
|
* {@link SKILL_FIELD_SHAPES})**同一只**:一边判一边不判 = 同一个键两种话(S-377 baseDir 吃过的亏)。
|
|
50
49
|
*
|
|
51
|
-
*
|
|
52
|
-
* `B` / `a` / `s` / `h` 四条
|
|
53
|
-
* (`*` / 写路径 / 域名
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
50
|
+
* 本谓词只判「字符串数组」(core 7.42.0 起与请求的 `settings.permissions.allow`、运维的 `APPROVAL_GRANT` **同一只**:`rule-entry-list.ts` 的
|
|
51
|
+
* `ruleEntryListShapeOk`):引擎直接 `for (const entry of list)` —— 一个裸字符串 `"Bash"` 会被逐字符迭代成 `B` / `a` / `s` / `h` 四条
|
|
52
|
+
* (deny 侧 = 意图静默落空,安全轴上的放宽方向)。条目的**文法与名字**是引擎的读法:允侧读不出的条目(`*` / 写路径 / 域名 / 退役名 …)引擎在开跑时发
|
|
53
|
+
* `permission.allowed_tool_not_granted`(受众 = 用户);拒侧读成空的条目(文法或名字层)在 `resolveSpec` 拒(提交 400 / 续跑 409,`rule-entry-list.ts` 的
|
|
54
|
+
* `skillDenyInertEntryOf`,判据 = 引擎的条目读法)。形对之后两道门再问 {@link skillToolListRefusal}(条数帽 + 引擎读不到的那一形 ⇒ 400)。
|
|
55
|
+
* 条数帽 = 条目表那一只(`rule-entry-list.ts` `allowLayerEntryCountIssue`,准备开销的界;与请求 allow 桶、运维表同一只),
|
|
56
|
+
* 在 {@link skillToolListRefusal} 里判;无长度帽([ref])。
|
|
57
57
|
*/
|
|
58
|
-
export declare const skillToolListOk: (v: unknown) =>
|
|
59
|
-
/** 技能的两只清单(`allowedTools` = allow 规则来源,`disallowedTools` = deny 规则来源;条目的工具名那一段两只同读)。 */
|
|
60
|
-
declare const SKILL_TOOL_LISTS: readonly ["allowedTools", "disallowedTools"];
|
|
61
|
-
/** 技能清单里一条够不着任何挂载工具的协议名条目。 */
|
|
62
|
-
export interface SkillToolListUnreachable {
|
|
63
|
-
readonly key: (typeof SKILL_TOOL_LISTS)[number];
|
|
64
|
-
readonly entry: string;
|
|
65
|
-
readonly verdict: UnreachableNamespacedRuleName;
|
|
66
|
-
}
|
|
58
|
+
export declare const skillToolListOk: (v: unknown) => v is readonly string[];
|
|
67
59
|
/**
|
|
68
|
-
* 一条技能(形已过两道门的字符串数组判)
|
|
69
|
-
* (
|
|
70
|
-
*
|
|
71
|
-
*
|
|
60
|
+
* 一条技能(形已过两道门的字符串数组判)的两只清单在受理口的拒因 —— 都过 ⇒ `undefined`。两处受理口同调:顶层 `skills[]`
|
|
61
|
+
* (`http/admission.ts` 的 `validateUserSkills`)、子代 `agents[].skills[]`({@link validateTaskAgents})。两问:条数帽;引擎读不到、却永远匹配不到的
|
|
62
|
+
* 条目(工具段为空的协议名,`rule-entry-list.ts` 那一份,与配置表、会话规则写口同一只)。退役名 / 对端段铸不出 / 读成空的条目是引擎的读法(见上):
|
|
63
|
+
* 允侧交引擎通告,拒侧由 `resolveSpec` 拒。
|
|
72
64
|
*/
|
|
73
|
-
export declare function
|
|
74
|
-
/** 两处受理口同一句(句子的后半与配置表 / 会话规则写口同一只 `describeUnreachableNamespacedRuleName`)。 */
|
|
75
|
-
export declare function describeUnreachableSkillToolListEntry(u: SkillToolListUnreachable): string;
|
|
65
|
+
export declare function skillToolListRefusal(skill: Record<string, unknown>): string | undefined;
|
|
76
66
|
/** [ref] 单条 agent 定义的形状裁决:null=合法;否则一句可 400 的英文错误(不带索引,调用方自己加)。
|
|
77
67
|
* 白名单外键 fail-loud —— 与 resilience/limits 的「容忍未知键」相反:agents 是能力定义,静默丢字段 =
|
|
78
68
|
* 定义被悄悄削(一个丢掉的 isolation 就是放宽),比 400 更危险,故整条拒。⚠️ 已知残账(core 半场):
|
|
@@ -188,5 +178,4 @@ export declare function normalizeApproachNotice(raw: unknown): false | {
|
|
|
188
178
|
* 全空 ⇒ undefined(spec 不挂 limits 键,byte-compat)。
|
|
189
179
|
*/
|
|
190
180
|
export declare function resolveTaskLimits(bodyLimits: unknown, caps: TaskLimitCaps, envTimeoutSec: number, requirePrincipal: boolean | undefined, big: boolean, warn?: (event: string, fields: Record<string, unknown>) => void): TaskSpec["limits"];
|
|
191
|
-
export {};
|
|
192
181
|
//# sourceMappingURL=spec-fields.d.ts.map
|
package/dist/spec-fields.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isThinkingLevel } from "@sema-agent/core";
|
|
2
2
|
import { taskWallClockSec } from "./task-workflow.js";
|
|
3
3
|
import { isValidCwd } from "./task-cwd.js";
|
|
4
|
-
import {
|
|
4
|
+
import { allowLayerEntryCountIssue, describeEmptyToolSegmentEntry, emptyToolSegmentEntryOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
|
|
5
5
|
export function normalizeSuggestNextPrompts(v) {
|
|
6
6
|
if (v === true)
|
|
7
7
|
return true;
|
|
@@ -66,26 +66,22 @@ const toolList = (v) => Array.isArray(v) &&
|
|
|
66
66
|
v.length <= MAX_AGENT_TOOLS &&
|
|
67
67
|
v.every((x) => typeof x === "string" && x.length > 0 && x.length <= MAX_AGENT_TOOL_NAME_CHARS);
|
|
68
68
|
const plainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
69
|
-
export const skillToolListOk =
|
|
69
|
+
export const skillToolListOk = ruleEntryListShapeOk;
|
|
70
70
|
const SKILL_TOOL_LISTS = ["allowedTools", "disallowedTools"];
|
|
71
|
-
export function
|
|
71
|
+
export function skillToolListRefusal(skill) {
|
|
72
72
|
for (const key of SKILL_TOOL_LISTS) {
|
|
73
73
|
const list = skill[key];
|
|
74
|
-
if (!
|
|
74
|
+
if (!ruleEntryListShapeOk(list))
|
|
75
75
|
continue;
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
76
|
+
const over = allowLayerEntryCountIssue(`skill.${key}`, list);
|
|
77
|
+
if (over !== undefined)
|
|
78
|
+
return over.message;
|
|
79
|
+
const empty = emptyToolSegmentEntryOf(list);
|
|
80
|
+
if (empty !== undefined)
|
|
81
|
+
return describeEmptyToolSegmentEntry(`skill.${key}`, empty);
|
|
83
82
|
}
|
|
84
83
|
return undefined;
|
|
85
84
|
}
|
|
86
|
-
export function describeUnreachableSkillToolListEntry(u) {
|
|
87
|
-
return `skill.${u.key} names "${u.entry}", which can never match a tool (${describeUnreachableNamespacedRuleName(u.verdict)})`;
|
|
88
|
-
}
|
|
89
85
|
const skillFilesOk = (v) => Array.isArray(v) &&
|
|
90
86
|
v.every((f) => plainObject(f) && typeof f.path === "string" && f.path.length > 0 && typeof f.content === "string");
|
|
91
87
|
const SKILL_FIELD_SHAPES = {
|
|
@@ -197,9 +193,9 @@ export function validateTaskAgents(v) {
|
|
|
197
193
|
const skills = v[i].skills;
|
|
198
194
|
if (Array.isArray(skills)) {
|
|
199
195
|
for (let j = 0; j < skills.length; j++) {
|
|
200
|
-
const
|
|
201
|
-
if (
|
|
202
|
-
return `agents[${i}]: skills[${j}]: ${
|
|
196
|
+
const refusal = skillToolListRefusal(skills[j]);
|
|
197
|
+
if (refusal !== undefined)
|
|
198
|
+
return `agents[${i}]: skills[${j}]: ${refusal}`;
|
|
203
199
|
}
|
|
204
200
|
}
|
|
205
201
|
const name = v[i].name;
|
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
/** 读口的入参:任何一种载着「这一问」的形(活的 `AskRequest`、`ToolCallRequest
|
|
1
|
+
/** 读口的入参:任何一种载着「这一问」的形(活的 `AskRequest`、`ToolCallRequest`、停驻行的 `tool_approval` 臂)都结构上满足。 */
|
|
2
2
|
export interface StandingAnswerGrantCarrier {
|
|
3
3
|
readonly toolName?: unknown;
|
|
4
4
|
readonly boundGrantDigest?: unknown;
|
|
5
5
|
}
|
|
6
|
-
/**
|
|
6
|
+
/** 一问的授予绑定(闭集):`none` = 应答只覆盖这一次调用;`bound` = 授予、绑定可读;`unreadable` = 授予、绑定读不出。 */
|
|
7
|
+
export type GrantBinding = {
|
|
8
|
+
readonly kind: "none";
|
|
9
|
+
} | {
|
|
10
|
+
readonly kind: "bound";
|
|
11
|
+
readonly digest: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "unreadable";
|
|
14
|
+
};
|
|
15
|
+
/** 这一问的授予绑定(读法见文件头注)。 */
|
|
16
|
+
export declare function grantBindingOf(carrier: StandingAnswerGrantCarrier): GrantBinding;
|
|
17
|
+
/** 这一问的应答是否授予这一次调用之外的东西(= 绑定不是 `none`)。**按工具名记**的读者(耐久豁免店的读写口、豁免列表)据它不碰这类问:
|
|
18
|
+
* 它们的键没有绑定这一维,记下 / 代答都会越过绑定。 */
|
|
7
19
|
export declare function answerGrantsBeyondCall(carrier: StandingAnswerGrantCarrier): boolean;
|
|
8
20
|
//# sourceMappingURL=standing-answer-grant.d.ts.map
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { SKILL_TOOL_NAME } from "@sema-agent/core";
|
|
2
|
+
export function grantBindingOf(carrier) {
|
|
3
|
+
if (Object.hasOwn(carrier, "boundGrantDigest")) {
|
|
4
|
+
const d = carrier.boundGrantDigest;
|
|
5
|
+
return typeof d === "string" && d.length > 0 ? { kind: "bound", digest: d } : { kind: "unreadable" };
|
|
6
|
+
}
|
|
7
|
+
return carrier.toolName === SKILL_TOOL_NAME ? { kind: "unreadable" } : { kind: "none" };
|
|
8
|
+
}
|
|
2
9
|
export function answerGrantsBeyondCall(carrier) {
|
|
3
|
-
return
|
|
10
|
+
return grantBindingOf(carrier).kind !== "none";
|
|
4
11
|
}
|
|
5
12
|
//# sourceMappingURL=standing-answer-grant.js.map
|
package/dist/task-settings.d.ts
CHANGED
|
@@ -12,22 +12,21 @@
|
|
|
12
12
|
* idempotent (the same tighten), so no special resume handling is needed (mirrors runtime-governance).
|
|
13
13
|
*
|
|
14
14
|
* 🔒 SECURITY INVARIANTS (SDK settings.ts §3.3) —— **scope: the `settings.permissions` channel this module projects, and only it**:
|
|
15
|
-
* -
|
|
16
|
-
* LOOSEN a stricter base safety field. Through
|
|
17
|
-
* deployment + operator baseline
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* `test/skill-grant-operator-knobs.test.ts`.
|
|
15
|
+
* - `deny` / `ask` are folded via `tightenTaskSpec` (deny-wins `combinePolicies`); it THROWS on any override that would
|
|
16
|
+
* LOOSEN a stricter base safety field. Through those two buckets the client can only ADD denies / asks / handsReadOnly on
|
|
17
|
+
* top of the deployment + operator baseline.
|
|
18
|
+
* - `allow` (server 7.117.0, core 7.42.0) is NOT a policy: the list rides WHOLE and verbatim into `TaskSpec.allowedTools`,
|
|
19
|
+
* the request member of the engine's ONE allow layer (beside a deployment's `RunnerDeps.allowedTools`, a person's stored
|
|
20
|
+
* rows, an active skill's `allowed-tools` and a PreToolUse hook's `allow`), read under one predicate. It IS an accepted
|
|
21
|
+
* loosening for this run: an entry clears the `unstated` / `ordinary` questions the engine raises for the calls it covers
|
|
22
|
+
* (the built-in write gate and the shell's ordinary asks included), on hosted / multi-tenant deployments too — no
|
|
23
|
+
* deployment switch, by ruling (the execution lane is the sandbox; the operator's tools are the per-tool knobs). It clears
|
|
24
|
+
* no deny / ask rule (request or operator), no governance / shell-floor / write-protection / plan-mode / probe / mandated
|
|
25
|
+
* ask and no PreToolUse hook's ask, and opens no write containment; an entry that matches and cannot clear is disclosed on
|
|
26
|
+
* the card (`allowRuleShadowed`). An operator who needs a person asked for a given tool regardless uses the per-tool knobs:
|
|
27
|
+
* `APPROVAL_REQUIRE` (an ask rule), `APPROVAL_NEVER_AUTO` (a person, mandated), `MANUAL_MODE_SHELL_GATE=always` (shell).
|
|
28
|
+
* A request-carried skill's `allowedTools` is the same member's twin (same grammar, same predicate, same admission judge
|
|
29
|
+
* `rule-entry-list.ts`). Pinned on the real assembly: `test/allow-layer-operator-knobs.test.ts`.
|
|
31
30
|
* - permissionMode(轴B #2 注释保鲜,1.254——[ref]/[ref] 后的真实现):`defaultMode` 收**五**模式
|
|
32
31
|
* (default | acceptEdits | plan | bypassPermissions | auto),bypass/auto 被 HONOR 为 gate-shape 选择
|
|
33
32
|
* ——但深层不变量未变:bypass=「不加 ask 门」,恒不越部署基线(tightenTaskSpec deny-wins,基线 deny/
|
|
@@ -265,8 +264,9 @@ export interface SettingsPermissionBucketIssue {
|
|
|
265
264
|
}
|
|
266
265
|
/**
|
|
267
266
|
* 三桶的**唯一**判据(纯函数)。`perms` 不是对象 ⇒ 不判(那一形归「settings.permissions must be an object」那道门);桶缺席 / `null`
|
|
268
|
-
* ⇒ 不判(与 `permissions: null` 的「容器缺席」同一约定)。逐桶按 {@link SETTINGS_PERMISSION_BUCKETS}
|
|
269
|
-
* `index`)
|
|
267
|
+
* ⇒ 不判(与 `permissions: null` 的「容器缺席」同一约定)。逐桶按 {@link SETTINGS_PERMISSION_BUCKETS} 的序、按该桶的条目规则:不是数组 ⇒ `bad_entry`
|
|
268
|
+
* (无 `index`);`rule_policy` 桶超帽 ⇒ `over_cap`(先判,拒体有界)、首个非串 / 空串项 ⇒ `bad_entry` + 它的下标;`allow_layer` 桶首个非串项 ⇒
|
|
269
|
+
* `bad_entry` + 它的下标(整表判据 = `ruleEntryListShapeOk`),超帽同样 `over_cap`(帽的理由不同,见段头)。全合法 ⇒ `null`。
|
|
270
270
|
*/
|
|
271
271
|
export declare function settingsPermissionBucketIssue(perms: unknown): SettingsPermissionBucketIssue | null;
|
|
272
272
|
/** 本 service 真正**消费**的 `settings` 顶层键 —— 闭集。每一个都有消费点:
|
package/dist/task-settings.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import { combinePolicies, createFsWriteGatePolicy, parseRuleText,
|
|
1
|
+
import { combinePolicies, createFsWriteGatePolicy, parseRuleText, resolveReasoningProfile, rankOf, isThinkingLevel, CYBER_RISK, URL_SAFETY, HARNESS_SECTION_ANCHOR, RUN_WORKFLOW_TOOL_NAME, PERMISSION_MODES, ruleToolGrammarOf, } from "@sema-agent/core";
|
|
2
2
|
import { unmatchableToolNamesRefusal } from "./config.js";
|
|
3
3
|
import { MAX_PERMISSION_RULES_PER_LIST, createRuleListPolicy } from "./permission-rule-list.js";
|
|
4
|
+
import { allowLayerEntryCountIssue, describeAllowLayerOverCap, describeEmptyToolSegmentEntry, emptyToolSegmentEntryOf, ruleEntryListShapeOk } from "./rule-entry-list.js";
|
|
5
|
+
import { tightenGoverned } from "./governance-overrides.js";
|
|
4
6
|
import { MAX_KEY_ECHO, keyClosureClause, scanKeyClosure } from "./request-key-closure.js";
|
|
5
7
|
import { parseHooksConfig } from "./hooks/hook-runner.js";
|
|
6
8
|
import { heldRuleName } from "./tool-rule-names.js";
|
|
9
|
+
import { policyAskStandingAnswerFacts, standingAnswerDurablyRecordable } from "./tool-approval.js";
|
|
7
10
|
export const PERMISSION_MODE_REQUEST_FACE = {
|
|
8
11
|
default: "request",
|
|
9
12
|
acceptEdits: "request",
|
|
@@ -83,15 +86,28 @@ function cleanEnvMap(v) {
|
|
|
83
86
|
}
|
|
84
87
|
export const SETTINGS_PERMISSION_BUCKETS = ["allow", "deny", "ask"];
|
|
85
88
|
export const SETTINGS_PERMISSION_BUCKET_REFUSALS = ["over_cap", "bad_entry"];
|
|
86
|
-
|
|
89
|
+
const BUCKET_ENTRY_RULE = { allow: "allow_layer", deny: "rule_policy", ask: "rule_policy" };
|
|
90
|
+
function bucketIssueMessage(f, rule) {
|
|
87
91
|
switch (f.reason) {
|
|
88
92
|
case "over_cap":
|
|
89
|
-
|
|
90
|
-
|
|
93
|
+
switch (rule) {
|
|
94
|
+
case "rule_policy":
|
|
95
|
+
return (`${f.field} has ${f.count} entries — at most ${f.cap} are accepted. Refused rather than truncated: a truncated list silently drops the rules past the cap, ` +
|
|
96
|
+
`and a dropped deny / ask rule is a tightening that never happened. Merge rules into broader patterns or trim the list`);
|
|
97
|
+
case "allow_layer":
|
|
98
|
+
return describeAllowLayerOverCap(f.field, f.count);
|
|
99
|
+
}
|
|
91
100
|
case "bad_entry":
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
101
|
+
switch (rule) {
|
|
102
|
+
case "rule_policy":
|
|
103
|
+
return f.index === undefined
|
|
104
|
+
? `${f.field} must be an array of non-empty rule strings — refused rather than ignored, because an ignored list reads as "in effect" to the caller`
|
|
105
|
+
: `${f.field}[${f.index}] is not a non-empty rule string — every entry must be one. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`;
|
|
106
|
+
case "allow_layer":
|
|
107
|
+
return f.index === undefined
|
|
108
|
+
? `${f.field} must be an array of permission-rule strings — refused rather than ignored, because an ignored list reads as "in effect" to the caller`
|
|
109
|
+
: `${f.field}[${f.index}] is not a string — every entry must be a permission-rule string. Refused rather than dropped, because a dropped entry reads as "in effect" to the caller`;
|
|
110
|
+
}
|
|
95
111
|
}
|
|
96
112
|
}
|
|
97
113
|
export function settingsPermissionBucketIssue(perms) {
|
|
@@ -102,18 +118,34 @@ export function settingsPermissionBucketIssue(perms) {
|
|
|
102
118
|
if (v === undefined || v === null)
|
|
103
119
|
continue;
|
|
104
120
|
const field = `settings.permissions.${bucket}`;
|
|
121
|
+
const rule = BUCKET_ENTRY_RULE[bucket];
|
|
105
122
|
let fields;
|
|
106
123
|
if (!Array.isArray(v))
|
|
107
124
|
fields = { field, reason: "bad_entry" };
|
|
108
|
-
else if (v.length > MAX_PERMISSION_RULES_PER_LIST)
|
|
109
|
-
fields = { field, reason: "over_cap", count: v.length, cap: MAX_PERMISSION_RULES_PER_LIST };
|
|
110
125
|
else {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
switch (rule) {
|
|
127
|
+
case "rule_policy": {
|
|
128
|
+
if (v.length > MAX_PERMISSION_RULES_PER_LIST)
|
|
129
|
+
fields = { field, reason: "over_cap", count: v.length, cap: MAX_PERMISSION_RULES_PER_LIST };
|
|
130
|
+
else {
|
|
131
|
+
const index = v.findIndex((x) => typeof x !== "string" || x.length === 0);
|
|
132
|
+
if (index >= 0)
|
|
133
|
+
fields = { field, reason: "bad_entry", index };
|
|
134
|
+
}
|
|
135
|
+
break;
|
|
136
|
+
}
|
|
137
|
+
case "allow_layer": {
|
|
138
|
+
const over = allowLayerEntryCountIssue(field, v);
|
|
139
|
+
if (over !== undefined)
|
|
140
|
+
fields = { field, reason: "over_cap", count: over.count, cap: over.cap };
|
|
141
|
+
else if (!ruleEntryListShapeOk(v))
|
|
142
|
+
fields = { field, reason: "bad_entry", index: v.findIndex((x) => typeof x !== "string") };
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
114
146
|
}
|
|
115
147
|
if (fields !== undefined)
|
|
116
|
-
return { message: bucketIssueMessage(fields), fields };
|
|
148
|
+
return { message: bucketIssueMessage(fields, rule), fields };
|
|
117
149
|
}
|
|
118
150
|
return null;
|
|
119
151
|
}
|
|
@@ -265,7 +297,7 @@ export function acceptAppendSystemPrompt(v, warn, packDropsAppend) {
|
|
|
265
297
|
}
|
|
266
298
|
return v;
|
|
267
299
|
}
|
|
268
|
-
const RULE_SOURCE_BY_BEHAVIOR = {
|
|
300
|
+
const RULE_SOURCE_BY_BEHAVIOR = { ask: "settings.permissions.ask", deny: "settings.permissions.deny" };
|
|
269
301
|
const WORKFLOW_ASK_NAMES = [RUN_WORKFLOW_TOOL_NAME];
|
|
270
302
|
const isWorkflowName = (name) => name === RUN_WORKFLOW_TOOL_NAME;
|
|
271
303
|
function createWorkflowAskPolicy(isExempt) {
|
|
@@ -273,7 +305,8 @@ function createWorkflowAskPolicy(isExempt) {
|
|
|
273
305
|
async check(req) {
|
|
274
306
|
if (heldRuleName(req, isWorkflowName) === undefined)
|
|
275
307
|
return { action: "allow" };
|
|
276
|
-
|
|
308
|
+
const ask = { action: "ask", message: `dynamic workflow "${req.toolName}" requires approval in this permission mode`, decisionReason: "rule" };
|
|
309
|
+
if (isExempt && standingAnswerDurablyRecordable(policyAskStandingAnswerFacts(req, ask))) {
|
|
277
310
|
try {
|
|
278
311
|
if (await isExempt(req.toolName))
|
|
279
312
|
return { action: "allow", decisionReason: "rule" };
|
|
@@ -281,7 +314,7 @@ function createWorkflowAskPolicy(isExempt) {
|
|
|
281
314
|
catch {
|
|
282
315
|
}
|
|
283
316
|
}
|
|
284
|
-
return
|
|
317
|
+
return ask;
|
|
285
318
|
},
|
|
286
319
|
nameSets: [{ ask: WORKFLOW_ASK_NAMES }],
|
|
287
320
|
};
|
|
@@ -301,7 +334,6 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
|
|
|
301
334
|
for (const [field, names, behavior] of [
|
|
302
335
|
["permissions.ask", perms.ask, "ask"],
|
|
303
336
|
["permissions.deny", perms.deny, "deny"],
|
|
304
|
-
["permissions.allow", perms.allow, "allow"],
|
|
305
337
|
]) {
|
|
306
338
|
for (const name of names ?? []) {
|
|
307
339
|
const parsed = parseRuleText(name, behavior);
|
|
@@ -313,30 +345,31 @@ function rejectUnmatchableSettingsNames(perms, pathBases) {
|
|
|
313
345
|
}
|
|
314
346
|
}
|
|
315
347
|
}
|
|
316
|
-
|
|
317
|
-
["permissions.ask", perms.ask, "ask"],
|
|
318
|
-
["permissions.deny", perms.deny, "deny"],
|
|
319
|
-
|
|
320
|
-
]) {
|
|
348
|
+
const judgments = [
|
|
349
|
+
["permissions.ask", perms.ask, { consumer: "rule-policy", behavior: "ask", ...(pathBases !== undefined ? { pathBases } : {}) }],
|
|
350
|
+
["permissions.deny", perms.deny, { consumer: "rule-policy", behavior: "deny", ...(pathBases !== undefined ? { pathBases } : {}) }],
|
|
351
|
+
];
|
|
352
|
+
for (const [field, names, judgment] of judgments) {
|
|
321
353
|
if (!names)
|
|
322
354
|
continue;
|
|
323
|
-
const unmatchable = unmatchableToolNamesRefusal(field, names,
|
|
355
|
+
const unmatchable = unmatchableToolNamesRefusal(field, names, judgment);
|
|
324
356
|
if (unmatchable !== undefined)
|
|
325
357
|
throw new Error(unmatchable.message);
|
|
326
358
|
}
|
|
359
|
+
const emptyAllow = perms.allow !== undefined ? emptyToolSegmentEntryOf(perms.allow) : undefined;
|
|
360
|
+
if (emptyAllow !== undefined)
|
|
361
|
+
throw new Error(describeEmptyToolSegmentEntry("permissions.allow", emptyAllow));
|
|
327
362
|
}
|
|
328
363
|
export function deriveSettingsPolicy(settings, gate, workflowGate) {
|
|
329
364
|
const perms = settings.permissions;
|
|
330
365
|
if (!perms)
|
|
331
366
|
return {};
|
|
332
|
-
const {
|
|
367
|
+
const { deny, ask, defaultMode } = perms;
|
|
333
368
|
const pathBases = gate?.taskRoot !== undefined || gate?.taskHome !== undefined
|
|
334
369
|
? { ...(gate.taskRoot !== undefined ? { root: gate.taskRoot } : {}), ...(gate.taskHome !== undefined ? { home: gate.taskHome } : {}) }
|
|
335
370
|
: undefined;
|
|
336
371
|
rejectUnmatchableSettingsNames(perms, pathBases);
|
|
337
372
|
const policies = [];
|
|
338
|
-
if (allow)
|
|
339
|
-
policies.push(createRuleListPolicy(allow, "allow", RULE_SOURCE_BY_BEHAVIOR.allow, pathBases));
|
|
340
373
|
if (deny)
|
|
341
374
|
policies.push(createRuleListPolicy(deny, "deny", RULE_SOURCE_BY_BEHAVIOR.deny, pathBases));
|
|
342
375
|
if (ask)
|
|
@@ -380,15 +413,13 @@ export function applyTaskSettings(base, settings, gate, workflowGate) {
|
|
|
380
413
|
const { toolPolicy, handsReadOnly, enablePlanMode } = deriveSettingsPolicy(settings, gate, workflowGate);
|
|
381
414
|
let next = base;
|
|
382
415
|
if (toolPolicy !== undefined || handsReadOnly !== undefined) {
|
|
383
|
-
|
|
384
|
-
if (toolPolicy !== undefined)
|
|
385
|
-
overrides.toolPolicy = toolPolicy;
|
|
386
|
-
if (handsReadOnly !== undefined)
|
|
387
|
-
overrides.handsReadOnly = handsReadOnly;
|
|
388
|
-
next = tightenTaskSpec(next, overrides);
|
|
416
|
+
next = tightenGoverned(next, { ...(toolPolicy !== undefined ? { toolPolicy } : {}), ...(handsReadOnly !== undefined ? { handsReadOnly } : {}) });
|
|
389
417
|
}
|
|
390
418
|
if (enablePlanMode)
|
|
391
419
|
next = { ...next, enablePlanMode: true };
|
|
420
|
+
const allow = settings.permissions?.allow;
|
|
421
|
+
if (allow !== undefined)
|
|
422
|
+
next = { ...next, allowedTools: allow };
|
|
392
423
|
if (settings.outputStyle) {
|
|
393
424
|
const appended = next.appendSystemPrompt ? `${next.appendSystemPrompt}\n\n${settings.outputStyle}` : settings.outputStyle;
|
|
394
425
|
next = { ...next, appendSystemPrompt: appended };
|