@sema-agent/core 7.9.0 → 7.9.2
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 +39 -0
- package/dist/agents/child-model-seat.d.ts +51 -0
- package/dist/agents/child-model-seat.js +34 -0
- package/dist/agents/subagent.d.ts +4 -2
- package/dist/agents/subagent.js +16 -12
- package/dist/core/ask-origin.d.ts +7 -3
- package/dist/core/checkpoint-store.d.ts +2 -7
- package/dist/core/effective-path-target.d.ts +43 -0
- package/dist/core/effective-path-target.js +56 -0
- package/dist/core/engine-notice.d.ts +8 -0
- package/dist/core/fs-write-gate-policy.js +2 -1
- package/dist/core/gate-lanes.d.ts +1 -0
- package/dist/core/gate-lanes.js +11 -5
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +10 -1
- package/dist/core/mcp-injection-drop.d.ts +74 -0
- package/dist/core/mcp-injection-drop.js +27 -0
- package/dist/core/permission-rule-model.d.ts +91 -75
- package/dist/core/permission-rule-model.js +90 -111
- package/dist/core/permission-rule-org.d.ts +12 -6
- package/dist/core/permission-rule-org.js +9 -3
- package/dist/core/permission-rules.d.ts +3 -2
- package/dist/core/permission-rules.js +38 -15
- package/dist/core/persisted-rule-arms.d.ts +7 -2
- package/dist/core/persisted-rule-arms.js +3 -1
- package/dist/core/roles.d.ts +8 -0
- package/dist/core/runner/active-skill-scope.js +2 -1
- package/dist/core/runner/contracts.d.ts +3 -2
- package/dist/core/runner/permission-rule-lanes.d.ts +12 -4
- package/dist/core/runner/permission-rule-lanes.js +13 -15
- package/dist/core/runner/prepare-caps-and-workflow.js +2 -2
- package/dist/core/runner/runtask.d.ts +2 -1
- package/dist/core/runner/runtask.js +1 -1
- package/dist/core/runner/session-rule-policy.js +2 -1
- package/dist/core/runner/tool-face-overlay.js +22 -3
- package/dist/core/sensitive-path-policy.js +5 -3
- package/dist/core/shell-lexer.d.ts +47 -0
- package/dist/core/shell-lexer.js +478 -0
- package/dist/core/shell-scan.d.ts +60 -0
- package/dist/core/shell-scan.js +183 -0
- package/dist/core/shell-wrapper-table.d.ts +297 -0
- package/dist/core/shell-wrapper-table.js +58 -0
- package/dist/core/tool-catalog-entries.js +6 -6
- package/dist/core/tool-face.d.ts +80 -4
- package/dist/core/tool-face.js +10 -0
- package/dist/core/tool-policy.d.ts +1 -6
- package/dist/core/tool-registry.d.ts +8 -11
- package/dist/core/tool-registry.js +5 -2
- package/dist/core/tool-roster.d.ts +11 -2
- package/dist/core/tool-roster.js +21 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/orchestration/workflow.js +21 -4
- package/dist/tools/fs/fs-search-tools.d.ts +3 -2
- package/dist/tools/fs/fs-search-tools.js +17 -9
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/fs/search.d.ts +0 -8
- package/dist/tools/fs/search.js +0 -23
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +57 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeForDisclosure, parseRuleText, ruleReachesProgramRun } from "./permission-rule-model.js";
|
|
1
|
+
import { escapeForDisclosure, parseRuleText, programRunReachOf, ruleReachesProgramRun } from "./permission-rule-model.js";
|
|
2
2
|
import { writerOf } from "./permission-rule-store.js";
|
|
3
3
|
export const ORG_UNAVAILABLE_DECISION_REASON = "org_unavailable";
|
|
4
4
|
export const ORG_ADJUDICATION_TIMEOUT_MS = 15_000;
|
|
@@ -244,19 +244,25 @@ function validateOrgSnapshot(s, nowMs) {
|
|
|
244
244
|
}
|
|
245
245
|
export function orgRuleVerdictFor(rules, call) {
|
|
246
246
|
let ask;
|
|
247
|
+
let unreadable;
|
|
247
248
|
for (const r of rules) {
|
|
248
249
|
const parsed = parseRuleText(r.rule, r.behavior);
|
|
249
250
|
if ("reject" in parsed)
|
|
250
251
|
continue;
|
|
251
252
|
if (parsed.rule.tool !== call.tool)
|
|
252
253
|
continue;
|
|
253
|
-
|
|
254
|
+
const outcome = programRunReachOf(parsed.rule, call.command);
|
|
255
|
+
if (outcome.reach === "not_reached")
|
|
254
256
|
continue;
|
|
257
|
+
if (outcome.reach === "unreadable") {
|
|
258
|
+
unreadable ??= { behavior: "ask", rule: r.rule, unreadable: outcome.reason };
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
255
261
|
if (r.behavior === "deny")
|
|
256
262
|
return { behavior: "deny", rule: r.rule };
|
|
257
263
|
ask ??= { behavior: "ask", rule: r.rule };
|
|
258
264
|
}
|
|
259
|
-
return ask;
|
|
265
|
+
return unreadable ?? ask;
|
|
260
266
|
}
|
|
261
267
|
export function orgRuleShadows(orgRules, rule) {
|
|
262
268
|
return compileOrgShadowPredicate(orgRules)(rule);
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
* this compiler now compiles it AS SUCH for the two tightening behaviors: the text goes through the ONE
|
|
17
17
|
* validator (`parseRuleText(text, behavior)` — the tightening floor, so `Bash(node:*)` as a deny is a
|
|
18
18
|
* legitimately wide tightening) and the call is judged with the ONE tightening reach
|
|
19
|
-
* (`
|
|
20
|
-
* per-subcommand deny, the same instrument the org partition and the personal store's deny/ask rows use
|
|
19
|
+
* (`programRunReachOf`: the whole command line or any program run in a connector chain — upstream's
|
|
20
|
+
* per-subcommand deny, the same instrument the org partition and the personal store's deny/ask rows use;
|
|
21
|
+
* a call the lexer cannot read against a rule is a fail-closed ask, never silence).
|
|
21
22
|
* It sits in the field-level tier of the evaluation order — right after the bare deny (resp. ask) — which
|
|
22
23
|
* is upstream's placement (`bs()` whole-tool deny → `AN(…,"deny")` field-level deny → `ph()` whole-tool
|
|
23
24
|
* ask → `AN(…,"ask")` field-level ask → allow). A content-form ALLOW is refused (`unsupported.bash_prefix`):
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
|
|
2
2
|
import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
|
|
3
3
|
import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
|
|
4
|
-
import { isUsablePathBase,
|
|
5
|
-
import {
|
|
4
|
+
import { isUsablePathBase, parseRuleText, pathRuleReaches, programRunReachOf } from "./permission-rule-model.js";
|
|
5
|
+
import { effectivePathTargetOf } from "./effective-path-target.js";
|
|
6
|
+
import { protectivePathTargetOf } from "./tool-registry.js";
|
|
6
7
|
export { parsePermissionRule };
|
|
7
8
|
export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
|
|
8
9
|
const DEFAULT_CAPS = {
|
|
@@ -317,18 +318,34 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
317
318
|
const coveringHit = (lane, toolName) => lane.length === 0 ? undefined : lane.find((r) => namespacedRuleNameCovers(r.ruleName, toolName));
|
|
318
319
|
const hasCovering = namespacedCovering.deny.length > 0 || namespacedCovering.ask.length > 0 || namespacedCovering.allow.length > 0;
|
|
319
320
|
const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
|
|
320
|
-
const cwdBase = (pathBases.cwd ?? pathBases.root ?? "").replace(/\/+$/, "");
|
|
321
321
|
const contentHit = (lane, req) => {
|
|
322
322
|
if (lane.length === 0)
|
|
323
323
|
return undefined;
|
|
324
|
-
const
|
|
325
|
-
const spelled = pt !== undefined ? pathTargetValue(req.args, pt) : undefined;
|
|
326
|
-
const target = spelled === undefined ? undefined : lexicalNormalAbsolutePathOf(spelled.startsWith("/") ? spelled : `${cwdBase}/${spelled}`);
|
|
324
|
+
const target = effectivePathTargetOf(req, protectivePathTargetOf(req), pathBases);
|
|
327
325
|
const command = isPlainObjectArgs(req.args) ? req.args[commandParam] : undefined;
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
326
|
+
let unreadable;
|
|
327
|
+
for (const r of lane) {
|
|
328
|
+
if (r.parsed.match === "subpath" || r.parsed.match === "path") {
|
|
329
|
+
if (target !== undefined && pathRuleReaches(r.parsed, target, pathBases))
|
|
330
|
+
return { rule: r };
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
if (typeof command !== "string")
|
|
334
|
+
continue;
|
|
335
|
+
const outcome = programRunReachOf(r.parsed, command);
|
|
336
|
+
if (outcome.reach === "reached")
|
|
337
|
+
return { rule: r };
|
|
338
|
+
if (outcome.reach === "unreadable")
|
|
339
|
+
unreadable ??= { rule: r, unreadable: outcome.reason };
|
|
340
|
+
}
|
|
341
|
+
return unreadable;
|
|
331
342
|
};
|
|
343
|
+
const unreadableAsk = (hit, req) => ({
|
|
344
|
+
action: "ask",
|
|
345
|
+
message: `tool "${req.toolName}" needs approval: the command could not be read against the permission rule ${hit.rule.ruleText} (${hit.unreadable}) — a person must decide`,
|
|
346
|
+
matchedAskRule: hit.rule.ruleText,
|
|
347
|
+
requiresRealApproval: true,
|
|
348
|
+
});
|
|
332
349
|
return {
|
|
333
350
|
nameSets: [nameSets],
|
|
334
351
|
check(req) {
|
|
@@ -342,15 +359,19 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
342
359
|
if (coveringDeny)
|
|
343
360
|
return { action: "deny", message: ruleMessage("denied", coveringDeny.ruleText, coveringDeny.source) };
|
|
344
361
|
}
|
|
362
|
+
const contentDeny = entry !== undefined ? contentHit(entry.content.deny, req) : undefined;
|
|
363
|
+
if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
|
|
364
|
+
return { action: "deny", message: ruleMessage("denied", contentDeny.rule.ruleText, contentDeny.rule.source) };
|
|
365
|
+
}
|
|
345
366
|
if (entry) {
|
|
346
|
-
const contentDeny = contentHit(entry.content.deny, req);
|
|
347
|
-
if (contentDeny) {
|
|
348
|
-
return { action: "deny", message: ruleMessage("denied", contentDeny.ruleText, contentDeny.source) };
|
|
349
|
-
}
|
|
350
367
|
const paramDeny = matchParamRules(entry.param.deny, req.args, caps.maxScalarValueChars);
|
|
351
368
|
if (paramDeny) {
|
|
352
369
|
return { action: "deny", message: ruleMessage("denied", paramDeny.ruleText, paramDeny.source) };
|
|
353
370
|
}
|
|
371
|
+
}
|
|
372
|
+
if (contentDeny?.unreadable !== undefined)
|
|
373
|
+
return unreadableAsk({ rule: contentDeny.rule, unreadable: contentDeny.unreadable }, req);
|
|
374
|
+
if (entry) {
|
|
354
375
|
if (entry.bare.ask) {
|
|
355
376
|
return { action: "ask", message: ruleMessage("flagged", entry.bare.ask.ruleText, entry.bare.ask.source), matchedAskRule: entry.bare.ask.ruleText };
|
|
356
377
|
}
|
|
@@ -362,8 +383,10 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
362
383
|
}
|
|
363
384
|
if (entry) {
|
|
364
385
|
const contentAsk = contentHit(entry.content.ask, req);
|
|
365
|
-
if (contentAsk) {
|
|
366
|
-
|
|
386
|
+
if (contentAsk !== undefined) {
|
|
387
|
+
if (contentAsk.unreadable !== undefined)
|
|
388
|
+
return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, req);
|
|
389
|
+
return { action: "ask", message: ruleMessage("flagged", contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
|
|
367
390
|
}
|
|
368
391
|
const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
|
|
369
392
|
if (paramAsk) {
|
|
@@ -18,6 +18,9 @@ import type { PersistedRuleHit, PersistedRuleHitRule } from "./hooks.js";
|
|
|
18
18
|
export interface PersistedRuleRead {
|
|
19
19
|
readonly hit?: PersistedRuleHit;
|
|
20
20
|
readonly unreadable?: true;
|
|
21
|
+
/** The lexer's reason when the CALL could not be read against the person's deny/ask rows (absent for
|
|
22
|
+
* a store read failure) — the message says which it was; the decision is the same. */
|
|
23
|
+
readonly reason?: string;
|
|
21
24
|
}
|
|
22
25
|
/** Display form of a rule set (design/375 §5.1): every rule text is a DISPLAY value on a human trust
|
|
23
26
|
* boundary, and the set must stay readable however large a lane makes it — each member rides
|
|
@@ -42,8 +45,10 @@ export declare function disclosedRuleSet(rules: readonly PersistedRuleHitRule[])
|
|
|
42
45
|
* ask, the marker is stamped if absent and the message names the rule — the standing question
|
|
43
46
|
* outranks a remembered yes across lanes, exactly as the DSL's own order says within one; a deny
|
|
44
47
|
* stays a deny.
|
|
45
|
-
* · UNREADABLE — the wired store could not be read (a read failure, a timeout)
|
|
46
|
-
*
|
|
48
|
+
* · UNREADABLE — the wired store could not be read (a read failure, a timeout), or the CALL could not be
|
|
49
|
+
* read against the person's standing deny/ask rows (`reason` carries the lexer's word: an expansion
|
|
50
|
+
* where a rule reads a word, an unterminated quote, a syntax error): the person's deny/ask rows
|
|
51
|
+
* cannot be enforced for this call, so it fails CLOSED the way an unreadable org snapshot does — an
|
|
47
52
|
* allow tightens to an ask carrying `requiresRealApproval` (no automatic lane clears it; the gate
|
|
48
53
|
* derives the origin `rule_store_unavailable` from its own record of the read), an ask acquires the
|
|
49
54
|
* bit, a deny stays a deny. Reading an unreadable store as "no rule" would erase every deny the
|
|
@@ -8,7 +8,9 @@ export function applyPersistedTightening(decision, read) {
|
|
|
8
8
|
if (read?.unreadable === true) {
|
|
9
9
|
if (decision.action === "deny")
|
|
10
10
|
return { decision };
|
|
11
|
-
const message =
|
|
11
|
+
const message = read.reason !== undefined
|
|
12
|
+
? `this call could not be read against the person's standing deny/ask rules (${read.reason}) — whether one of them forbids it is unknown, so it needs a real approval`
|
|
13
|
+
: "the persisted permission-rule store could not be read for this call — the person's own deny/ask rules cannot be enforced, so this call needs a real approval until the store is readable";
|
|
12
14
|
return {
|
|
13
15
|
decision: decision.action === "allow"
|
|
14
16
|
? { action: "ask", message, decisionReason: "persisted_rule", requiresRealApproval: true, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) }
|
package/dist/core/roles.d.ts
CHANGED
|
@@ -98,6 +98,14 @@ export interface ResolvedRole {
|
|
|
98
98
|
* Resolve the model (and default thinking) for a task. Explicit `spec.model` wins; otherwise walk the
|
|
99
99
|
* `modelRole`'s fallback chain over the merged roles (task overrides runner) and use the first role
|
|
100
100
|
* that supplies a model. Throws a helpful error if nothing resolves.
|
|
101
|
+
*
|
|
102
|
+
* @contract roles.side_channels_deployment_only — an engine SIDE CHANNEL (the auto-mode classifier,
|
|
103
|
+
* the compaction/summarize lane, memory consolidation, next-prompt suggestion) resolves here with a
|
|
104
|
+
* `modelRole` and NO run model: it runs on the deployment's roles alone — the boot default at the end of
|
|
105
|
+
* its chain — and never follows the model a run was switched to. Only a run's CHILDREN follow the parent:
|
|
106
|
+
* the delegation seat (`childModelSeat`) passes the parent's `Model` as `spec.model` with the role kept
|
|
107
|
+
* on `modelRole`, so the role's presets still apply. A side channel that must follow a run passes that
|
|
108
|
+
* run's model explicitly; nothing here infers it.
|
|
101
109
|
*/
|
|
102
110
|
export declare function resolveTaskModel(spec: {
|
|
103
111
|
model?: ModelRef;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { canonicalizeTarget } from "../../tools/fs/safety.js";
|
|
2
2
|
import { pathTargetValue } from "../tool-registry.js";
|
|
3
|
+
import { pathTargetBaseOf } from "../effective-path-target.js";
|
|
3
4
|
import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
|
|
4
5
|
import { parseSkillToolEntry, skillSpecifierRejection } from "../skill-tool-specifier.js";
|
|
5
6
|
export class ActiveSkillScope {
|
|
@@ -99,7 +100,7 @@ export function createActiveSkillScopePolicy(opts) {
|
|
|
99
100
|
decisionReason: "safety",
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
|
-
const canon = await canonicalizeTarget(env, path, signal, req.cwd
|
|
103
|
+
const canon = await canonicalizeTarget(env, path, signal, pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
|
|
103
104
|
if (!canon.ok) {
|
|
104
105
|
return {
|
|
105
106
|
action: "deny",
|
|
@@ -43,7 +43,7 @@ import type { SystemInjectionPriority, TaskNotificationPayload } from "../task-n
|
|
|
43
43
|
import type { OnAsk, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
|
|
44
44
|
import type { RemoteExecutionEnv, SnapshotId, WorkspaceHandle } from "../remote-env.js";
|
|
45
45
|
import type { ToolDisclosureManifest } from "../trace.js";
|
|
46
|
-
import type { AgentDefinition, Brain, CompactOutcome, NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
|
|
46
|
+
import type { AgentDefinition, Brain, CompactOutcome, ModelRoles, NestedUsageAccum, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
|
|
47
47
|
import type { WiringManifest } from "../wiring-manifest.js";
|
|
48
48
|
import type { ActiveSkillFrame } from "./active-skill-scope.js";
|
|
49
49
|
import type { BudgetAxis } from "./assemble-result.js";
|
|
@@ -2377,11 +2377,12 @@ export interface RunnerDepsSeat {
|
|
|
2377
2377
|
export interface RunnerSelfSeat {
|
|
2378
2378
|
/** The session store this Runner was constructed over — the delegation tools acquire / release / fork through it. */
|
|
2379
2379
|
readonly sessions: SessionStore;
|
|
2380
|
-
/** The deployment agent + model catalog, READ-ONLY (a fresh copy per read; the definitions are the shared objects). */
|
|
2380
|
+
/** The deployment agent + model catalog, READ-ONLY (a fresh copy per read; the definitions are the shared objects); `roles` = `RunnerDeps.roles` (the delegation seat's `childModelSeat` reads `roles.subagent`). */
|
|
2381
2381
|
readonly agentCatalog: {
|
|
2382
2382
|
agents?: AgentDefinition[];
|
|
2383
2383
|
builtinAgents?: boolean;
|
|
2384
2384
|
models?: Record<string, Model>;
|
|
2385
|
+
roles?: ModelRoles;
|
|
2385
2386
|
};
|
|
2386
2387
|
/** The deployment-level gating baseline (deps toolPolicy / hooks) as delegate wrappers — for assemblers that compose with it. */
|
|
2387
2388
|
readonly gateBaseline: {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* silence the ungated-write warning for deployments that wired no policy at all.
|
|
27
27
|
*/
|
|
28
28
|
import type { AskRuleEvidence, ToolCallRequest } from "../tool-policy.js";
|
|
29
|
-
import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit } from "../hooks.js";
|
|
29
|
+
import { persistedRuleMandateOf, type OrgGateVerdict, type PersistedRuleAnswer, type PersistedRuleHit, type PersistedRuleUnreadable } from "../hooks.js";
|
|
30
30
|
import type { PersistedRule, PersistedRuleVerdict, RuleOffer, SegmentCoverage } from "../permission-rule-model.js";
|
|
31
31
|
import { type OrgRuleResolution } from "../permission-rule-org.js";
|
|
32
32
|
import type { PermissionRuleStoreProvider } from "../permission-rule-provider.js";
|
|
@@ -79,11 +79,19 @@ export declare function orgRevisionEvidenceOf(resolution: OrgRuleResolution, onD
|
|
|
79
79
|
* aliased: the evidence must not change under a store that reuses its row objects.
|
|
80
80
|
*/
|
|
81
81
|
export declare function persistedRuleHitOf(verdict: PersistedRuleVerdict | undefined): PersistedRuleHit | undefined;
|
|
82
|
+
/** The COMMAND arm's projection of a verdict: a call the lexer could not read against a standing deny/ask
|
|
83
|
+
* row is the lane's UNREADABLE answer — the same fail-closed shape an unreadable store takes (a
|
|
84
|
+
* real-approval ask no automatic lane clears), with the lexer's reason in place of the store's — never a
|
|
85
|
+
* matched ask rule, which would let a blanket `onAsk: "allow"` clear a question nobody can answer. Every
|
|
86
|
+
* other verdict is {@link persistedRuleHitOf}'s hit. */
|
|
87
|
+
export declare function persistedRuleAnswerOf(verdict: PersistedRuleVerdict | undefined): PersistedRuleHit | PersistedRuleUnreadable | undefined;
|
|
82
88
|
/**
|
|
83
89
|
* design/382 §2.5-1, widened to every path-targeting tool — the persisted-rule lane's PATH arm: the call's
|
|
84
|
-
* target path in the SAME lexical identity the rule family is defined over
|
|
85
|
-
*
|
|
86
|
-
*
|
|
90
|
+
* target path in the SAME lexical identity the rule family is defined over, resolved by the ONE reader every
|
|
91
|
+
* fence and every tool goes through (`effectivePathTargetOf`), so the lane judges the path the tool will
|
|
92
|
+
* really open. The tool's own declaration decides which base a relative spelling takes and what an ABSENT
|
|
93
|
+
* slot means — the lane no longer guesses `liveCwd ?? root` for every tool, which was wrong for the two
|
|
94
|
+
* search tools in three separate shapes (see effective-path-target.ts). Still zero IO.
|
|
87
95
|
*
|
|
88
96
|
* TWO readings of the call's path slot, one per direction (design/388 B6/B17): the tightening arms judge
|
|
89
97
|
* the PROTECTIVE target (the object face's declaration, the catalog's for the name as the floor — reading
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { persistedRuleMandateOf } from "../hooks.js";
|
|
2
|
-
import { adjudicatePersistedPathRules, adjudicatePersistedRules,
|
|
3
|
-
import {
|
|
2
|
+
import { adjudicatePersistedPathRules, adjudicatePersistedRules, ruleToolGrammarOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
3
|
+
import { effectivePathTargetOf } from "../effective-path-target.js";
|
|
4
|
+
import { declaredPathTargetOf, protectivePathTargetOf } from "../tool-registry.js";
|
|
4
5
|
import { orgRuleVerdictFor } from "../permission-rule-org.js";
|
|
5
6
|
export const PERSISTED_RULE_TOOL = "Bash";
|
|
6
7
|
const HANDOFF_BOUND = 256;
|
|
@@ -24,21 +25,18 @@ export function persistedRuleHitOf(verdict) {
|
|
|
24
25
|
? undefined
|
|
25
26
|
: { behavior: verdict.behavior, rules: verdict.rules.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
|
|
26
27
|
}
|
|
28
|
+
export function persistedRuleAnswerOf(verdict) {
|
|
29
|
+
if (verdict?.unreadable !== undefined)
|
|
30
|
+
return { unreadable: true, reason: verdict.unreadable };
|
|
31
|
+
return persistedRuleHitOf(verdict);
|
|
32
|
+
}
|
|
27
33
|
export function pathRuleLaneAnswer(table, req, ctx) {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
return undefined;
|
|
32
|
-
const spelled = pathTargetValue(req.args, pt);
|
|
33
|
-
if (spelled === undefined)
|
|
34
|
-
return undefined;
|
|
35
|
-
return lexicalNormalAbsolutePathOf(spelled.startsWith("/") ? spelled : `${base}/${spelled}`);
|
|
36
|
-
};
|
|
37
|
-
const tighten = resolve(protectivePathTargetOf(req));
|
|
38
|
-
const allow = resolve(declaredPathTargetOf(req));
|
|
34
|
+
const bases = { ...(ctx.root !== undefined ? { root: ctx.root } : {}), ...(ctx.liveCwd !== undefined ? { cwd: ctx.liveCwd } : {}) };
|
|
35
|
+
const tighten = effectivePathTargetOf(req, protectivePathTargetOf(req), bases);
|
|
36
|
+
const allow = effectivePathTargetOf(req, declaredPathTargetOf(req), bases);
|
|
39
37
|
if (tighten === undefined && allow === undefined)
|
|
40
38
|
return undefined;
|
|
41
|
-
return persistedRuleHitOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) },
|
|
39
|
+
return persistedRuleHitOf(adjudicatePersistedPathRules(table, { tool: req.toolName, cwd: ctx.root, sessionId: ctx.sessionId }, { ...(tighten !== undefined ? { tighten } : {}), ...(allow !== undefined ? { allow } : {}) }, bases));
|
|
42
40
|
}
|
|
43
41
|
export function createPermissionRuleLanes(cfg) {
|
|
44
42
|
const provider = cfg.provider;
|
|
@@ -137,7 +135,7 @@ export function createPermissionRuleLanes(cfg) {
|
|
|
137
135
|
const execCwd = liveCwd !== undefined ? { execCwd: liveCwd } : {};
|
|
138
136
|
const verdict = adjudicatePersistedRules(table, { tool: req.toolName, command, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
|
|
139
137
|
if (verdict !== undefined)
|
|
140
|
-
return
|
|
138
|
+
return persistedRuleAnswerOf(verdict);
|
|
141
139
|
const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: cfg.root, sessionId: cfg.sessionId, ...execCwd });
|
|
142
140
|
return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
|
|
143
141
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { isSyntheticApiErrorMessage } from "../../internal/harness.js";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
|
-
import { forkGovernanceDenial } from "../../agents/subagent.js";
|
|
3
|
+
import { deploymentSubagentTierModel, forkGovernanceDenial } from "../../agents/subagent.js";
|
|
4
4
|
import { CROSS_SESSION_CLASSIFIER_RULE } from "../../agents/cross-session-envelope.js";
|
|
5
5
|
import { createRunWorkflowTool } from "../../orchestration/run-workflow-tool.js";
|
|
6
6
|
import { isSelfOrchestrationActive } from "../../orchestration/workflow-script-runner.js";
|
|
@@ -243,7 +243,7 @@ export async function prepareCapsAndWorkflow(input) {
|
|
|
243
243
|
taskOwner: hostTaskId,
|
|
244
244
|
limits: deps.workflowLimits,
|
|
245
245
|
sourceTaskId: hostTaskId,
|
|
246
|
-
parentModel: () => harnessRef.current?.getModel(),
|
|
246
|
+
parentModel: () => deploymentSubagentTierModel(runnerSelf.agentCatalog) ?? harnessRef.current?.getModel(),
|
|
247
247
|
...(spec.getApiKeyAndHeaders !== undefined ? { parentGetApiKeyAndHeaders: spec.getApiKeyAndHeaders } : {}),
|
|
248
248
|
...(frozenOnAsk !== undefined ? { parentOnAsk: frozenOnAsk } : {}),
|
|
249
249
|
principal: spec.principal,
|
|
@@ -3,7 +3,7 @@ import { type CheckpointToken, type ResumeOutcome } from "../checkpoint-store.js
|
|
|
3
3
|
import { type TaskOutcome } from "../task-outcome.js";
|
|
4
4
|
import { type SideQuerySpec, type SideQueryResult } from "../side-query.js";
|
|
5
5
|
import type { SessionStore } from "../session.js";
|
|
6
|
-
import type { AgentDefinition, ModelRef, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
|
|
6
|
+
import type { AgentDefinition, ModelRef, ModelRoles, RunnerDeps, TaskEvent, TaskResult, TaskSpec, TaskStream } from "../types.js";
|
|
7
7
|
import type { ResumeRun, ResumeTaskConfig, RunInternals, RunnerSelfSeat } from "./contracts.js";
|
|
8
8
|
export type { ResumeTaskConfig } from "./contracts.js";
|
|
9
9
|
/**
|
|
@@ -74,6 +74,7 @@ export declare class Runner implements RunnerSelfSeat {
|
|
|
74
74
|
agents?: AgentDefinition[];
|
|
75
75
|
builtinAgents?: boolean;
|
|
76
76
|
models?: Record<string, Model>;
|
|
77
|
+
roles?: ModelRoles;
|
|
77
78
|
};
|
|
78
79
|
/** design/125 (实现审 B1) — the deployment-level gating baseline, exposed READ-ONLY so thin
|
|
79
80
|
* assemblers over the Runner (`runSpec`) can COMPOSE with it. The task-level engine semantic is a
|
|
@@ -138,7 +138,7 @@ export class Runner {
|
|
|
138
138
|
return {
|
|
139
139
|
...(this.deps.agents ? { agents: [...this.deps.agents] } : {}),
|
|
140
140
|
...(this.deps.builtinAgents !== undefined ? { builtinAgents: this.deps.builtinAgents } : {}),
|
|
141
|
-
...(this.deps.models ? { models: { ...this.deps.models } } : {}),
|
|
141
|
+
...(this.deps.models ? { models: { ...this.deps.models } } : {}), ...(this.deps.roles ? { roles: { ...this.deps.roles } } : {}),
|
|
142
142
|
};
|
|
143
143
|
}
|
|
144
144
|
get gateBaseline() {
|
|
@@ -2,6 +2,7 @@ import { canonicalizeTarget, writeTargetPath } from "../../tools/fs/safety.js";
|
|
|
2
2
|
import { isWinFormPath } from "../../tools/fs/safety.js";
|
|
3
3
|
import { createCoarseCommandNamePolicy, namespacedCoveringEntries, namespacedCoveringHit } from "../tool-policy.js";
|
|
4
4
|
import { declaredPathTargetOf, isProtectedWrite, pathConfinableWriteToolNames, skillScopeWriteToolNames } from "../tool-registry.js";
|
|
5
|
+
import { pathTargetBaseOf } from "../effective-path-target.js";
|
|
5
6
|
export const PATH_WRITE_TOOLS = skillScopeWriteToolNames();
|
|
6
7
|
export const PATH_CONFINABLE_WRITE_TOOLS = pathConfinableWriteToolNames();
|
|
7
8
|
export function isWithin(root, p) {
|
|
@@ -65,7 +66,7 @@ export function createSessionRulePolicy(rules, opts) {
|
|
|
65
66
|
if (typeof path !== "string" || path.length === 0) {
|
|
66
67
|
return deny(`write tool "${req.toolName}" denied: session rule confines writes to allowDirs but the call has no resolvable path`);
|
|
67
68
|
}
|
|
68
|
-
const canon = await canonicalizeTarget(env, path, signal, req.cwd
|
|
69
|
+
const canon = await canonicalizeTarget(env, path, signal, pathTargetBaseOf(declared, { root: rootPath, cwd: req.cwd }));
|
|
69
70
|
if (!canon.ok) {
|
|
70
71
|
return deny(`write to "${path}" denied: its real target could not be resolved against the session-rule allowDirs`);
|
|
71
72
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MCP_NAMESPACE } from "../protocol-table.js";
|
|
2
2
|
import { mintNamespacePrefix } from "../protocol-naming.js";
|
|
3
|
-
import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_KEY_MAX_CHARS } from "../tool-face.js";
|
|
3
|
+
import { TOOL_APPROVAL_CARDS, TOOL_FAMILIES, TOOL_PATH_ABSENCES, TOOL_PATH_ACCESSES, TOOL_PATH_BASES, RENDER_HINT_ACTIVITY_MAX_CHARS, RENDER_HINT_MAX_CHARS, RENDER_HINT_MAX_LIST, TOOL_KEY_MAX_CHARS, } from "../tool-face.js";
|
|
4
4
|
import { inputKeysOf } from "../tool-roster.js";
|
|
5
5
|
import { deliverEngineNotice } from "../engine-notice.js";
|
|
6
6
|
export const APPROVAL_CARD_REQUIRED_KEYS = Object.freeze({
|
|
@@ -26,9 +26,28 @@ export function toolFaceProblem(face, schema) {
|
|
|
26
26
|
if (face.pathTarget !== undefined) {
|
|
27
27
|
if (typeof face.pathTarget.param !== "string" || !top.has(face.pathTarget.param))
|
|
28
28
|
return `pathTarget.param ${JSON.stringify(face.pathTarget.param)} is not a top-level property of the tool's schema`;
|
|
29
|
-
if (!
|
|
29
|
+
if (!TOOL_PATH_ACCESSES.includes(face.pathTarget.access))
|
|
30
30
|
return `pathTarget.access ${JSON.stringify(face.pathTarget.access)} is outside the closed set`;
|
|
31
|
-
const
|
|
31
|
+
const base = face.pathTarget.base;
|
|
32
|
+
if (base !== undefined && !TOOL_PATH_BASES.includes(base))
|
|
33
|
+
return `pathTarget.base ${JSON.stringify(base)} is outside the closed set`;
|
|
34
|
+
if (base === "root" && face.pathTarget.access !== "read")
|
|
35
|
+
return `pathTarget.base "root" needs pathTarget.access "read" — two write guards (the session-transcript directory guard and the frozen-spec deny) resolve a write target against the call's working directory only, so a write face declaring a root base would be judged on two different files (#638)`;
|
|
36
|
+
const absent = face.pathTarget.absent;
|
|
37
|
+
if (absent !== undefined && !TOOL_PATH_ABSENCES.includes(absent))
|
|
38
|
+
return `pathTarget.absent ${JSON.stringify(absent)} is outside the closed set`;
|
|
39
|
+
if (absent === "base" && face.pathTarget.access !== "read")
|
|
40
|
+
return `pathTarget.absent "base" needs pathTarget.access "read" — it says the call is about the whole base directory when the slot is empty, which is a search SCOPE; a write with no path names no file, and the containment fences judge a write on its own target`;
|
|
41
|
+
const patternParam = face.pathTarget.patternParam;
|
|
42
|
+
if (patternParam !== undefined) {
|
|
43
|
+
if (typeof patternParam !== "string" || !top.has(patternParam))
|
|
44
|
+
return `pathTarget.patternParam ${JSON.stringify(patternParam)} is not a top-level property of the tool's schema`;
|
|
45
|
+
if (absent !== "base")
|
|
46
|
+
return `pathTarget.patternParam ${JSON.stringify(patternParam)} is declared but pathTarget.absent is ${JSON.stringify(absent ?? "none")} — the pattern base is only ever read for an absent slot, so this declaration would be inert`;
|
|
47
|
+
}
|
|
48
|
+
const long = overlong("pathTarget.param", [face.pathTarget.param]) ??
|
|
49
|
+
overlong("pathTarget.aliases", face.pathTarget.aliases ?? []) ??
|
|
50
|
+
overlong("pathTarget.patternParam", patternParam === undefined ? [] : [patternParam]);
|
|
32
51
|
if (long !== undefined)
|
|
33
52
|
return long;
|
|
34
53
|
vocabulary.add(face.pathTarget.param);
|
|
@@ -2,7 +2,8 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { isAbsolute, join, resolve } from "node:path";
|
|
3
3
|
import { canonicalizeTarget, expandHomeTilde, isAbsolutePathForm, isWinFormPath, writeTargetPath } from "../tools/fs/safety.js";
|
|
4
4
|
import { compileSegmentPattern, matchSegmentPatterns } from "../tools/fs/read-deny.js";
|
|
5
|
-
import { isProtectedWrite } from "./tool-registry.js";
|
|
5
|
+
import { isProtectedWrite, protectivePathTargetOf } from "./tool-registry.js";
|
|
6
|
+
import { pathTargetBaseOf } from "./effective-path-target.js";
|
|
6
7
|
export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
7
8
|
".env",
|
|
8
9
|
".env.*",
|
|
@@ -97,10 +98,11 @@ export function createSensitivePathPolicy(opts) {
|
|
|
97
98
|
return { action: "allow" };
|
|
98
99
|
if (guardedByName !== undefined ? !guardedByName.has(req.toolName) : !isProtectedWrite(req))
|
|
99
100
|
return { action: "allow" };
|
|
100
|
-
const
|
|
101
|
+
const protective = protectivePathTargetOf(req);
|
|
102
|
+
const path = writeTargetPath(req, protective);
|
|
101
103
|
if (typeof path !== "string" || path.length === 0)
|
|
102
104
|
return { action: "allow" };
|
|
103
|
-
const canon = await canonicalizeTarget(opts.env, path, signal,
|
|
105
|
+
const canon = await canonicalizeTarget(opts.env, path, signal, pathTargetBaseOf(protective, { root: opts.rootPath, cwd: req.cwd }));
|
|
104
106
|
if (!canon.ok) {
|
|
105
107
|
if (canon.unresolvedSymlink) {
|
|
106
108
|
return {
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/** One argv word as the lexer read it. */
|
|
2
|
+
export interface ShellWord {
|
|
3
|
+
/** The word's value with quotes and escapes removed — meaningful only when `expands` is `false`. */
|
|
4
|
+
readonly text: string;
|
|
5
|
+
/** The unquoted source spelling (assignment and wrapper-flag tests read this, never `text`). */
|
|
6
|
+
readonly raw: string;
|
|
7
|
+
/** `false`: a literal word. `"one"`: carries an expansion but yields exactly ONE word (it was double-quoted).
|
|
8
|
+
* `"many"`: an unquoted expansion, glob or brace pattern, or a quoted `"$@"` / array expansion — may
|
|
9
|
+
* yield zero, one or several words. */
|
|
10
|
+
readonly expands: false | "one" | "many";
|
|
11
|
+
}
|
|
12
|
+
/** One program run the lexer found, after keyword stripping. */
|
|
13
|
+
export interface ShellSegment {
|
|
14
|
+
/** The run as SPELLED (a leading keyword removed): the first candidate a rule is compared against. */
|
|
15
|
+
readonly argv: readonly ShellWord[];
|
|
16
|
+
/** The deeper candidates, one per peeled layer (leading assignments, then each wrapper of the table):
|
|
17
|
+
* `sudo -u root rm -r x` ⇒ `[[rm, -r, x]]`. Empty when nothing peeled. */
|
|
18
|
+
readonly peeled: readonly (readonly ShellWord[])[];
|
|
19
|
+
/** Present when this segment's program runs cannot be known from the text at all; `argv` then holds
|
|
20
|
+
* whatever was read before the failure and must not be matched as a run. */
|
|
21
|
+
readonly unreadable?: string;
|
|
22
|
+
/** Present when the segment's program runs are NOT fully known although its candidates could be read:
|
|
23
|
+
* a peel that stopped before the wrapped program could be named (its options carry an expansion, a
|
|
24
|
+
* command string, an option of unknown arity), or a command/process substitution anywhere in the
|
|
25
|
+
* segment (it runs a program the lexer does not read). The candidates stand — a rule they reach is
|
|
26
|
+
* reached — and every other rule reads `unreadable`. */
|
|
27
|
+
readonly peelUnreadable?: string;
|
|
28
|
+
}
|
|
29
|
+
/** A command line as the tightening reader sees it. */
|
|
30
|
+
export interface ShellCommandShape {
|
|
31
|
+
readonly segments: readonly ShellSegment[];
|
|
32
|
+
/** The connector BETWEEN each consecutive pair of segments (`;` `&&` `||` `|` `|&` `&` `\n`). */
|
|
33
|
+
readonly connectors: readonly string[];
|
|
34
|
+
/** A subshell, group, control-structure keyword or dropped empty piece bounded the segments: the
|
|
35
|
+
* connector list is not one flat chain a compound rule could spell. */
|
|
36
|
+
readonly grouped: boolean;
|
|
37
|
+
}
|
|
38
|
+
export { SHELL_WRAPPER_TABLE, type ShellWrapperName } from "./shell-wrapper-table.js";
|
|
39
|
+
/** Longer than this and the command is not read at all (one unreadable segment): every pass is linear,
|
|
40
|
+
* but a substitution nested inside quoting re-enters the quote reader once per nesting level, and the
|
|
41
|
+
* cap is what bounds that on model-supplied text. Upstream's own reader stops at the same size. */
|
|
42
|
+
export declare const MAX_SHELL_READ_CHARS = 10000;
|
|
43
|
+
/** Read `source` as the tightening reader must: every program run the text could perform, or why it cannot be read. */
|
|
44
|
+
export declare function readShellCommand(source: string): ShellCommandShape;
|
|
45
|
+
/** Every program run of `shape` can be NAMED from the text: no segment is unreadable, no peel stopped,
|
|
46
|
+
* and every run candidate's program word is literal. The offer/mint side's "judgeable" question. */
|
|
47
|
+
export declare function isFullyReadable(shape: ShellCommandShape): boolean;
|