@sema-agent/core 7.42.1 → 7.43.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +100 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -3
- package/dist/agents/teacher.js +1 -1
- package/dist/agents/verify.js +2 -0
- package/dist/core/allow-entry.d.ts +50 -31
- package/dist/core/allow-entry.js +100 -37
- package/dist/core/ask-origin.d.ts +16 -5
- package/dist/core/ask-origin.js +11 -3
- package/dist/core/ask-unresolvable-notice.d.ts +3 -2
- package/dist/core/ask-unresolvable-notice.js +1 -1
- package/dist/core/auto-mode.d.ts +5 -4
- package/dist/core/auto-mode.js +3 -1
- package/dist/core/checkpoint-store.d.ts +18 -7
- package/dist/core/checkpoint-store.js +6 -3
- package/dist/core/child-clamp-seats.d.ts +15 -1
- package/dist/core/child-clamp-seats.js +6 -2
- package/dist/core/classifier-unavailable-arm.d.ts +42 -23
- package/dist/core/classifier-unavailable-arm.js +14 -3
- package/dist/core/decision-receipt.js +2 -0
- package/dist/core/gate-lanes.js +37 -29
- package/dist/core/hooks.d.ts +6 -1
- package/dist/core/hooks.js +3 -0
- package/dist/core/mode-policy.d.ts +5 -4
- package/dist/core/mode-policy.js +4 -0
- package/dist/core/permission-rule-consent.d.ts +3 -2
- package/dist/core/permission-rule-consent.js +25 -20
- package/dist/core/permission-rule-model.d.ts +47 -7
- package/dist/core/permission-rule-model.js +80 -6
- package/dist/core/permission-rules.d.ts +8 -2
- package/dist/core/permission-rules.js +26 -9
- package/dist/core/runner/classifier-ladder.d.ts +10 -8
- package/dist/core/runner/classifier-ladder.js +3 -1
- package/dist/core/runner/denial-limit-arms.d.ts +25 -8
- package/dist/core/runner/denial-limit-arms.js +24 -10
- package/dist/core/runner/gate-exit.js +2 -1
- package/dist/core/runner/permission-rule-lanes.d.ts +2 -2
- package/dist/core/runner/permission-rule-lanes.js +8 -2
- package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
- package/dist/core/runner/prepare-ask-lane.js +2 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -3
- package/dist/core/runner/prepare-caps-and-workflow.js +3 -2
- package/dist/core/runner/prepare-config-doors.d.ts +3 -0
- package/dist/core/runner/prepare-config-doors.js +6 -5
- package/dist/core/runner/prepare-gate-stations.js +1 -1
- package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
- package/dist/core/runner/prepare-inherited-gate.js +5 -3
- package/dist/core/runner/prepare-park-ask.d.ts +4 -4
- package/dist/core/runner/prepare-park-ask.js +2 -4
- package/dist/core/runner/prepare-policy-chain.d.ts +7 -1
- package/dist/core/runner/prepare-policy-chain.js +59 -38
- package/dist/core/runner/prepare-project-context.js +6 -2
- package/dist/core/runner/prepare-task.js +6 -6
- package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -5
- package/dist/core/runner/prepare-wiring-manifest.js +8 -9
- package/dist/core/runner/resume-checkpoint-screen.js +1 -1
- package/dist/core/runner/synthetic-tools.d.ts +3 -2
- package/dist/core/runner/synthetic-tools.js +1 -1
- package/dist/core/session-policy-store.d.ts +3 -3
- package/dist/core/session-policy-store.js +18 -4
- package/dist/core/skill-grant-binding.d.ts +44 -0
- package/dist/core/skill-grant-binding.js +22 -0
- package/dist/core/skill-grant.d.ts +59 -28
- package/dist/core/skill-grant.js +96 -23
- package/dist/core/task-spec.d.ts +14 -1
- package/dist/core/tighten-task-spec.d.ts +2 -0
- package/dist/core/tighten-task-spec.js +3 -0
- package/dist/core/tool-name-aliases.d.ts +35 -5
- package/dist/core/tool-name-aliases.js +29 -4
- package/dist/core/tool-policy.d.ts +10 -0
- package/dist/core/tool-policy.js +9 -2
- package/dist/core/tool-spec.d.ts +7 -0
- package/dist/core/wiring-manifest.d.ts +56 -14
- package/dist/core/wiring-manifest.js +29 -4
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/orchestration/run-workflow-tool.d.ts +4 -0
- package/dist/orchestration/run-workflow-tool.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +3 -1
- package/dist/orchestration/workflow-primitives.js +6 -6
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +11 -1
- package/wire-changes/core@7.43.0.json +985 -0
|
@@ -11,7 +11,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
|
|
|
11
11
|
import { mountWriteProtection } from "./write-protection-station.js";
|
|
12
12
|
import { autoRunOnRecommendationEffective } from "./memory-consolidation-driver.js";
|
|
13
13
|
import { GLOBAL_USAGE_KEY, usageRetryAfterMs, windowsGovernCost } from "../usage-window-store.js";
|
|
14
|
-
import { LSP_CONTRACT_ID, askSeatEffective, countElicitOptIns, deriveHandsSeam, deriveLspSeam, deriveWiringManifest,
|
|
14
|
+
import { LSP_CONTRACT_ID, askSeatEffective, countElicitOptIns, deriveHandsSeam, deriveLspSeam, deriveWiringManifest, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam, resolveSubagentTranscriptTier } from "../wiring-manifest.js";
|
|
15
15
|
import { modePolicyOf } from "../mode-policy.js";
|
|
16
16
|
import { DEFAULT_IRREVERSIBLE_SCOPE } from "./checkpoint-scope.js";
|
|
17
17
|
export function mcpManifestEntries(entries, statuses) {
|
|
@@ -102,9 +102,9 @@ function announceDurableGateUnavailable(args) {
|
|
|
102
102
|
message: `Durable approval gate mandated but unavailable: ${source === "force_durable_gate" ? "the runtime entitlement forceDurableGate is in force for this principal" : "this task opted into durable approval (durableApproval) with no live approver seat"}, ` +
|
|
103
103
|
`but ${cause === "task_store_disabled" ? 'this task disabled the checkpoint store (checkpointStore: "disabled")' : "this deployment wired no checkpoint store"}, ` +
|
|
104
104
|
`so no interactive ask of this task can be parked for a durable answer. Asks resolve on the live chain instead ` +
|
|
105
|
-
`(${args.liveApprover ? "a live approver answers permission asks in-stream" : args.blanketAllow ? 'a blanket "allow" posture answers ordinary permission asks without judgment (a mandated ask is refused)' : "permission asks are denied fail-closed —
|
|
105
|
+
`(${args.liveApprover ? "a live approver answers permission asks in-stream" : args.blanketAllow ? 'a blanket "allow" posture answers ordinary permission asks without judgment (a mandated ask is refused)' : "permission asks are denied fail-closed — nobody in this run can be asked to approve them"}; ` +
|
|
106
106
|
`${liveQuestionFace ? "a live question face answers AskUserQuestion in-stream" : args.liveQuestionFace ? "AskUserQuestion is withheld from the live face by an inherited durable mandate and refused" : "AskUserQuestion has no live face and is refused"}) ` +
|
|
107
|
-
`and NO durable approval record is written. ${cause === "task_store_disabled" ? "Run this task with its store" : "Wire a checkpoint store on this deployment"}, or ${source === "force_durable_gate" ? "withdraw the entitlement for principals served here" : "give the task a live approver seat (or drop durableApproval)"}.`,
|
|
107
|
+
`and NO durable approval record is written. ${cause === "task_store_disabled" ? "Run this task with its store" : "Wire a checkpoint store on this deployment"}, or ${source === "force_durable_gate" ? "withdraw the entitlement for principals served here" : args.declaredNone ? 'run the task where a person can be asked (without approvalReach: "none"), or drop durableApproval' : "give the task a live approver seat (or drop durableApproval)"}.`,
|
|
108
108
|
detail: {
|
|
109
109
|
sessionId: args.sessionId,
|
|
110
110
|
runId: args.runId,
|
|
@@ -117,8 +117,8 @@ function announceDurableGateUnavailable(args) {
|
|
|
117
117
|
});
|
|
118
118
|
}
|
|
119
119
|
export function prepareWiringManifest(input) {
|
|
120
|
-
const { spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, taskRootFinal, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, permissionMode, lockedPreflight, peerLaneRefs, frameSettlement, sessionBox, harness, pausedRef,
|
|
121
|
-
const onAsk =
|
|
120
|
+
const { spec, deps, internals, resume, sessionId, runId, hostTaskId, taskScope, hooks, preToolUseObservational, policyLayers, effectivePolicy, handsEnabled, taskRootFinal, mcp, irreversibleTools, egressTools, toolEffects, ownedEnv, executionEnv, offloadStore, onceLedger, usageWindows, envLifetimeSuspendAt, runtimeCaps, resolvedInteractionPosture, durableQuestionFace, liveQuestionFace, checkpointStore, sessions, modelGateManifest, autoModeArmReason, permissionMode, lockedPreflight, peerLaneRefs, frameSettlement, sessionBox, harness, pausedRef, askSeat, liveInheritedGate, delegation, toolRoster, readDenyBuiltinTiersActive, lspManagerWired, shellReadStationArmed, lspDiagnosticsLive, fileHistoryFact } = input;
|
|
121
|
+
const onAsk = askSeat.value;
|
|
122
122
|
const handWriteTools = toolRoster.entries.filter((e) => e.mountedBy === "hands" && (e.effect === "write" || e.effect === "idempotent")).map((e) => e.name);
|
|
123
123
|
const hasEffectAwareGate = Boolean(policyLayers.length > 0 || (hooks?.preToolUse !== undefined && !preToolUseObservational));
|
|
124
124
|
const peerWriteUngated = modePolicyOf(permissionMode.current).unstated === "ask"
|
|
@@ -181,14 +181,13 @@ export function prepareWiringManifest(input) {
|
|
|
181
181
|
const writeProtection = mountWriteProtection({ deps, env: handsEnabled ? executionEnv : undefined, rootPath: taskRootFinal, roster: toolRoster, sessionId, runId });
|
|
182
182
|
const wiringQuestionSeam = resolveQuestionSeam(spec, deps);
|
|
183
183
|
const wiringQuestionStripped = internals?.questionFaceStripped === true;
|
|
184
|
-
const wiringAskSeam = resolveAskSeamForm(spec, deps);
|
|
185
184
|
const wiringManifest = deriveWiringManifest({
|
|
186
185
|
half: "effective",
|
|
187
186
|
leg: resume !== undefined ? "resume" : internals?.isDelegatedChild === true ? "child" : "root",
|
|
188
187
|
teamRole: teamRoleOf({ isDelegatedChild: delegation.isDelegatedChild, ...(internals?.explicitAgentName !== undefined ? { spawnName: internals.explicitAgentName } : {}) }),
|
|
189
188
|
...(spec.peerPresence !== undefined ? { peerPresence: spec.peerPresence } : {}),
|
|
190
|
-
askForm:
|
|
191
|
-
...(
|
|
189
|
+
askForm: askSeat.form,
|
|
190
|
+
...(askSeat.provenance !== undefined ? { askProvenance: askSeat.provenance } : {}),
|
|
192
191
|
questionWired: wiringQuestionSeam.wired,
|
|
193
192
|
...(wiringQuestionSeam.provenance !== undefined ? { questionProvenance: wiringQuestionSeam.provenance } : {}),
|
|
194
193
|
...(wiringQuestionStripped ? { questionStrippedByEngine: true } : {}),
|
|
@@ -254,7 +253,7 @@ export function prepareWiringManifest(input) {
|
|
|
254
253
|
resourceSuspendEligible ||
|
|
255
254
|
platformSuspendArmed ||
|
|
256
255
|
spec.enablePlanMode === true;
|
|
257
|
-
announceDurableGateUnavailable({ onNotice: deps.onNotice, mandate: durableMandateSourceOf({ forceDurableGate: runtimeCaps?.forceDurableGate === true, durableApprovalDeclared: spec.durableApproval !== undefined, onAsk
|
|
256
|
+
announceDurableGateUnavailable({ onNotice: deps.onNotice, mandate: durableMandateSourceOf({ forceDurableGate: runtimeCaps?.forceDurableGate === true, durableApprovalDeclared: spec.durableApproval !== undefined, onAsk }), storeWired: checkpointStore !== undefined, taskStoreDisabled: spec.checkpointStore === "disabled", liveApprover: isLiveApproverSeat(onAsk), blanketAllow: onAsk === "allow", declaredNone: askSeat.reach === "none", liveQuestionFace: liveQuestionFace !== undefined, parentConstraints: liveInheritedGate?.parentConstraints, sessionId, runId, principal: spec.principal });
|
|
258
257
|
const hookIdentity = mintHookInvocationIdentity({
|
|
259
258
|
sessionId,
|
|
260
259
|
taskId: hostTaskId,
|
|
@@ -48,7 +48,7 @@ export function resumeCheckpointScreen(input) {
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
if (readFrozenSkillGrants(cp.state.skillGrants) === undefined)
|
|
51
|
-
throw new CheckpointError("checkpoint.invalid_outcome", "this checkpoint carries a skill-grant section (state.skillGrants) that does not read as grant rows — refusing to resume without the grant rows and refusals it carries
|
|
51
|
+
throw new CheckpointError("checkpoint.invalid_outcome", "this checkpoint carries a skill-grant section (state.skillGrants) that does not read as grant rows under this engine — a damaged section, or a row an earlier release wrote whose entry this release reads as nothing (an entry with a surrounding space, say) — refusing to resume without the grant rows and refusals it carries; the checkpoint stays pending: finish it on a worker of the release that parked it, or expire it", { reason: "skill_grants_damaged" });
|
|
52
52
|
}
|
|
53
53
|
if (realApprovalOrgFact(preCasGateBit) !== undefined && runner.deps.permissionRuleStore?.partitions.org !== true) {
|
|
54
54
|
throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (org ${String(realApprovalOrgFact(preCasGateBit))}, routed as ${String(preCasGateBit?.origin)}) and this worker has no org adjudication wiring (its permission-rule store carries no org partition) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
|
|
@@ -169,9 +169,10 @@ export declare function renderSkillLinesWithin(skills: ReadonlyArray<SkillListin
|
|
|
169
169
|
*
|
|
170
170
|
* A SUCCESSFUL activation writes the skill's two lists into the run's grant table (`grants`, origin `model`): its
|
|
171
171
|
* `allowed-tools` then run without asking and its `disallowed-tools` are refused for the rest of the run; an unknown skill
|
|
172
|
-
* throws first and writes nothing
|
|
172
|
+
* throws first and writes nothing, as does one whose `disallowed-tools` holds an entry that reads as nothing (the table
|
|
173
|
+
* refuses it: the body is not served, nothing is granted). Activating a skill that declares a list was itself a question (the gate reads this
|
|
173
174
|
* tool's seat off the call's arguments — `Execute skill: <name>`), so a write here always follows a statement. The tool
|
|
174
175
|
* runs ALONE in its batch: a call after it in the same message is judged after the rows exist.
|
|
175
176
|
*/
|
|
176
|
-
export declare function createSkillTool(skills: SkillLike[], grants
|
|
177
|
+
export declare function createSkillTool(skills: SkillLike[], grants: SkillGrantTable): AgentTool;
|
|
177
178
|
export {};
|
|
@@ -188,7 +188,7 @@ export function createSkillTool(skills, grants) {
|
|
|
188
188
|
`(Only skills registered with this deployment are loadable — other products' skills directories are not read. ` +
|
|
189
189
|
`If the user wants one of those here, read its source file and recreate its content for this deployment instead.)`);
|
|
190
190
|
}
|
|
191
|
-
grants
|
|
191
|
+
grants.activate(skill, "model");
|
|
192
192
|
const baseDir = skill.baseDir;
|
|
193
193
|
let body = baseDir !== undefined ? substituteSkillRoot(skill.content, baseDir) : skill.content;
|
|
194
194
|
if (skill.files !== undefined && skill.files.length > 0) {
|
|
@@ -53,7 +53,7 @@ export interface PutRulesOptions {
|
|
|
53
53
|
operator?: boolean;
|
|
54
54
|
}
|
|
55
55
|
/** The {@link SessionPolicyError.detail} of a tool-name refusal: which bucket, which entry, the judgment's word, and the
|
|
56
|
-
*
|
|
56
|
+
* spelling to write instead when the judgment has one (a retired name's 1:1 successor, an unmintable segment's minted name). */
|
|
57
57
|
export interface SessionRuleNameRefusal {
|
|
58
58
|
field: SessionRuleToolNameField;
|
|
59
59
|
name: string;
|
|
@@ -63,8 +63,8 @@ export interface SessionRuleNameRefusal {
|
|
|
63
63
|
/** Typed error from {@link SessionPolicyStore.putRules}. `conflict` = stale `expectedRev`; `loosen_forbidden`
|
|
64
64
|
* = a non-operator write tried to relax the rules; `corrupt` = a backend could not read the erase-surviving
|
|
65
65
|
* rev mark ({@link SessionPolicyStore.deleteBySession}) and refuses rather than stamp an unproven rev; the fourth
|
|
66
|
-
* code = a tool-name bucket names a retired
|
|
67
|
-
* present on that code and only on it. */
|
|
66
|
+
* code = a tool-name bucket names a retired name, a prefix-less `__` name or a peer segment no mint spells
|
|
67
|
+
* ({@link assertSessionRuleNamesCurrent}) — `detail` is present on that code and only on it. */
|
|
68
68
|
export declare class SessionPolicyError extends Error {
|
|
69
69
|
readonly code: "conflict" | "loosen_forbidden" | "corrupt" | "legacy_tool_name";
|
|
70
70
|
readonly detail?: SessionRuleNameRefusal | undefined;
|
|
@@ -40,10 +40,24 @@ export function assertSessionRuleNamesCurrent(rules, mountedToolNames = NO_DECLA
|
|
|
40
40
|
if (verdict.kind === "current")
|
|
41
41
|
continue;
|
|
42
42
|
const quoted = JSON.stringify(name);
|
|
43
|
-
const successor = verdict.kind === "
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const successor = verdict.kind === "unprefixed_namespace" ? undefined : verdict.currentName;
|
|
44
|
+
let message;
|
|
45
|
+
switch (verdict.kind) {
|
|
46
|
+
case "retired":
|
|
47
|
+
message = `session rules ${field} names ${quoted}, a retired tool name (${verdict.guidance}) — with it in place no run of this session could start, so nothing was written.${successor !== undefined ? ` Use the current name ${JSON.stringify(successor)}.` : ""} If this deployment mounts its own tool under ${quoted}, declare it in the store's mountedToolNames.`;
|
|
48
|
+
break;
|
|
49
|
+
case "unprefixed_namespace":
|
|
50
|
+
message = `session rules ${field} names ${quoted}, which carries the "__" namespace separator but no protocol prefix — no tool is named that way (protocol tools are named ${NAMESPACED_NAME_SHAPES}), and with it in place no run of this session could start, so nothing was written.`;
|
|
51
|
+
break;
|
|
52
|
+
case "unmintable_segment":
|
|
53
|
+
message = `session rules ${field} names ${quoted}, which can match no tool (${verdict.guidance}) — with it in place no run of this session could start, so nothing was written.${successor !== undefined ? ` Write ${JSON.stringify(successor)}.` : ""}`;
|
|
54
|
+
break;
|
|
55
|
+
default: {
|
|
56
|
+
const exhaustive = verdict;
|
|
57
|
+
return exhaustive;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new SessionPolicyError("legacy_tool_name", message, { field, name, verdict: verdict.kind, ...(successor !== undefined ? { currentName: successor } : {}) });
|
|
47
61
|
}
|
|
48
62
|
}
|
|
49
63
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a Skill card's approval is bound to, as ONE value: the skill the call names and the two lists approving it releases,
|
|
3
|
+
* in the rule grammar's canonical spelling (de-duplicated, sorted). Its hash is the `boundGrantDigest` a parked approval is
|
|
4
|
+
* judged by, so a reader holds the list a person approved and the binding that covers it as the same fact — the live
|
|
5
|
+
* `AskRequest`, the parked row and the inbox projection all carry it (minted by `skillGrantBindingMembers`).
|
|
6
|
+
*
|
|
7
|
+
* A leaf on purpose: the checkpoint projection reads it at its boundary and may not import the module that mints it (it imports only
|
|
8
|
+
* the hash).
|
|
9
|
+
*/
|
|
10
|
+
export interface BoundSkillGrants {
|
|
11
|
+
/** The skill's name as the loaded table spells it, or `null` when the call names no loaded skill (it grants nothing). */
|
|
12
|
+
readonly skill: string | null;
|
|
13
|
+
/** The skill's `allowed-tools` entries — what approving the activation lets be used without asking. */
|
|
14
|
+
readonly allowedTools: readonly string[];
|
|
15
|
+
/** The skill's `disallowed-tools` entries — what activating it refuses for the rest of the run. */
|
|
16
|
+
readonly disallowedTools: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A stored value read as a {@link BoundSkillGrants}, whole or not at all: a fresh copy of the three members and nothing else,
|
|
20
|
+
* or `undefined` when any member is not of its type. Every member is read ONCE, so the value screened is the value published.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readBoundSkillGrants(raw: unknown): BoundSkillGrants | undefined;
|
|
23
|
+
/** The card members a call to the engine's `Skill` tool carries on every face its card has — empty for any other call. */
|
|
24
|
+
export interface SkillGrantBindingMembers {
|
|
25
|
+
/** The hash of {@link boundGrants}: the binding a parked approval of the card is judged by. */
|
|
26
|
+
boundGrantDigest?: string;
|
|
27
|
+
boundGrants?: BoundSkillGrants;
|
|
28
|
+
}
|
|
29
|
+
/** The leg's binding factory ({@link SkillGrantBindingMembers} for a call over `args`), the one every ask-mint site spreads. */
|
|
30
|
+
export type GrantBindingOf = (call: {
|
|
31
|
+
readonly toolName: string;
|
|
32
|
+
}, args: unknown) => SkillGrantBindingMembers;
|
|
33
|
+
/**
|
|
34
|
+
* The binding members of a stored parked row, as the inbox projection publishes them: the digest when it is a string, and the lists
|
|
35
|
+
* only as the value that digest hashes — {@link readBoundSkillGrants} whole, and equal to the digest by hash. A pair that
|
|
36
|
+
* disagrees, or lists beside no digest, would show an approver one list while the approval binds another: the lists read as absent.
|
|
37
|
+
*/
|
|
38
|
+
export declare function readBoundBinding(row: {
|
|
39
|
+
readonly boundGrantDigest?: unknown;
|
|
40
|
+
readonly boundGrants?: unknown;
|
|
41
|
+
} | undefined): {
|
|
42
|
+
boundGrantDigest?: string;
|
|
43
|
+
boundGrants?: BoundSkillGrants;
|
|
44
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { boundInputHashOf } from "./canonical-json.js";
|
|
2
|
+
export function readBoundSkillGrants(raw) {
|
|
3
|
+
if (typeof raw !== "object" || raw === null)
|
|
4
|
+
return undefined;
|
|
5
|
+
const { skill, allowedTools: allowed, disallowedTools: disallowed } = raw;
|
|
6
|
+
if (skill !== null && typeof skill !== "string")
|
|
7
|
+
return undefined;
|
|
8
|
+
const entries = (list) => {
|
|
9
|
+
if (!Array.isArray(list))
|
|
10
|
+
return undefined;
|
|
11
|
+
const copy = Array.from(list);
|
|
12
|
+
return copy.every((e) => typeof e === "string") ? copy : undefined;
|
|
13
|
+
};
|
|
14
|
+
const allowedTools = entries(allowed);
|
|
15
|
+
const disallowedTools = entries(disallowed);
|
|
16
|
+
return allowedTools !== undefined && disallowedTools !== undefined ? { skill, allowedTools, disallowedTools } : undefined;
|
|
17
|
+
}
|
|
18
|
+
export function readBoundBinding(row) {
|
|
19
|
+
const digest = row?.boundGrantDigest;
|
|
20
|
+
const grants = readBoundSkillGrants(row?.boundGrants);
|
|
21
|
+
return { ...(typeof digest === "string" ? { boundGrantDigest: digest } : {}), ...(grants !== undefined && typeof digest === "string" && boundInputHashOf(grants) === digest ? { boundGrants: grants } : {}) };
|
|
22
|
+
}
|
|
@@ -2,7 +2,8 @@ import type { PermissionResult, ToolCallRequest } from "./tool-policy.js";
|
|
|
2
2
|
import type { PermissionMode } from "./ask-origin.js";
|
|
3
3
|
import { type PermissionSeatInputs, type PermissionSeatReading } from "./mode-policy.js";
|
|
4
4
|
import type { AgentTool } from "../internal/harness-types.js";
|
|
5
|
-
import
|
|
5
|
+
import type { SkillGrantBindingMembers } from "./skill-grant-binding.js";
|
|
6
|
+
import { type AllowEntryGateSeats, type AllowEntryInert, type AllowEntryJudgeContext, type CompiledAllowEntry } from "./allow-entry.js";
|
|
6
7
|
/** Who wrote a row: the `Skill` tool on a successful activation, prepare for a skill the person invoked by name, or a
|
|
7
8
|
* delegated child's seed from its parent. */
|
|
8
9
|
export declare const SKILL_GRANT_ORIGINS: readonly ["model", "user", "inherited"];
|
|
@@ -25,12 +26,12 @@ export interface SkillGrantRow {
|
|
|
25
26
|
readonly compiled: CompiledAllowEntry;
|
|
26
27
|
}
|
|
27
28
|
/** An entry the grammar reads as granting or refusing nothing. */
|
|
28
|
-
export
|
|
29
|
+
export type SkillInertEntry = {
|
|
29
30
|
readonly skill: string;
|
|
30
31
|
readonly list: "allowed-tools" | "disallowed-tools";
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
} & AllowEntryInert;
|
|
33
|
+
/** The names the run mounts, read when a list is — a skill is activated later than prepare, and the roster is read as it stands. */
|
|
34
|
+
export type MountedToolNames = () => ReadonlySet<string>;
|
|
34
35
|
/** The two lists a skill declares (the shape of a `SkillSpec`, read by name). */
|
|
35
36
|
export interface SkillGrantSource {
|
|
36
37
|
readonly name: string;
|
|
@@ -45,23 +46,19 @@ export interface SkillGrantSource {
|
|
|
45
46
|
* the person's invoked names are read through it ({@link seedSkillGrants}).
|
|
46
47
|
*/
|
|
47
48
|
export declare function skillNamedBy<T extends Pick<SkillGrantSource, "name">>(args: unknown, skills: readonly T[]): T | undefined;
|
|
48
|
-
/**
|
|
49
|
-
* The digest of what a Skill card disclosed — `Execute skill: <name>` and the lists approving it releases: the skill's name
|
|
50
|
-
* as the table spells it ({@link skillNamedBy} resolved it, so it equals the call's `skill` exactly) and its two lists in the
|
|
51
|
-
* rule grammar's canonical spelling (`Tool()` / `Tool(*)` read as the bare name), de-duplicated and sorted, hashed as the
|
|
52
|
-
* approval binding is hashed (`boundInputHashOf`). `undefined` — no skill named — digests as a nameless skill declaring
|
|
53
|
-
* nothing (activating it grants nothing). Any change to the name or to either list changes it; the body does not. The read
|
|
54
|
-
* point a parked Skill approval binds to (stored beside the row's input binding, compared before the resumed activation).
|
|
55
|
-
*/
|
|
49
|
+
/** The digest of what a Skill card disclosed — `Execute skill: <name>` and the lists approving it releases ({@link skillGrantBinding}). The read point a parked Skill approval binds to (stored beside the row's input binding, compared before the resumed activation). */
|
|
56
50
|
export declare function skillGrantDigest(skill: Pick<SkillGrantSource, "name" | "allowedTools" | "disallowedTools"> | undefined): string;
|
|
57
51
|
/**
|
|
58
|
-
* The
|
|
59
|
-
*
|
|
60
|
-
*
|
|
52
|
+
* The card members of a call to the engine's `Skill` tool — the ONE factory every ask spreads them from: the live
|
|
53
|
+
* `AskRequest` an approver is handed, an ancestor's, the parked row and (through the row) the inbox projection.
|
|
54
|
+
* `boundGrants` is the skill's name and canonical lists, `boundGrantDigest` its hash — the same value, so what a person was
|
|
55
|
+
* shown and what an approving resume is judged by cannot differ. The binding is a property of the CALL, whoever asked and
|
|
56
|
+
* however the question was worded. Empty for any other call, and when no skill is loaded (no engine `Skill` tool is
|
|
57
|
+
* mounted, so a call by that name is not an activation).
|
|
61
58
|
*/
|
|
62
|
-
export declare function
|
|
59
|
+
export declare function skillGrantBindingMembers(call: {
|
|
63
60
|
readonly toolName: string;
|
|
64
|
-
}, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string):
|
|
61
|
+
}, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string): SkillGrantBindingMembers;
|
|
65
62
|
/**
|
|
66
63
|
* The approving resume of a parked call, judged against the card it was asked under. A row is a `Skill` approval when it
|
|
67
64
|
* carries a digest (the park said so), or when the resume would run the engine's `Skill` tool on it (skills loaded, the
|
|
@@ -87,10 +84,10 @@ export declare function declaresSkillLists(skill: Pick<SkillGrantSource, "allowe
|
|
|
87
84
|
/**
|
|
88
85
|
* Compile one skill's two lists into rows (`origin` stamped on each) and the entries that read as nothing. An allow list
|
|
89
86
|
* holding `*` voids that list whole (every other entry on it is reported too: the list grants nothing — the safe direction).
|
|
90
|
-
* A `*` on the deny list reads as nothing by itself
|
|
91
|
-
*
|
|
87
|
+
* A `*` on the deny list reads as nothing by itself, like any deny entry that does — and a skill whose deny list holds one
|
|
88
|
+
* is not written at all ({@link SkillGrantTable.activate}).
|
|
92
89
|
*/
|
|
93
|
-
export declare function compileSkillGrant(skill: SkillGrantSource, origin: SkillGrantOrigin): {
|
|
90
|
+
export declare function compileSkillGrant(skill: SkillGrantSource, origin: SkillGrantOrigin, roster?: ReadonlySet<string>): {
|
|
94
91
|
rows: SkillGrantRow[];
|
|
95
92
|
inert: SkillInertEntry[];
|
|
96
93
|
};
|
|
@@ -98,8 +95,10 @@ export declare function compileSkillGrant(skill: SkillGrantSource, origin: Skill
|
|
|
98
95
|
export interface SkillGrantHit {
|
|
99
96
|
readonly skill: string;
|
|
100
97
|
readonly entries: readonly string[];
|
|
98
|
+
/** On a refusal only: the call activates `skill`, whose `disallowed-tools` entries `entries` read as nothing (why, each). */
|
|
99
|
+
readonly readsAsNothing?: readonly AllowEntryInert[];
|
|
101
100
|
}
|
|
102
|
-
/** The refusal a deny row
|
|
101
|
+
/** The refusal a deny hit writes: a deny row's, or an activation's whose deny entries read as nothing. */
|
|
103
102
|
export declare function skillDenyDecision(hit: SkillGrantHit): PermissionResult;
|
|
104
103
|
/** The run's deny rows over `req` through `lane` — the refusal they write, or undefined (no lane, no row). The one
|
|
105
104
|
* reading the gate's regular decision, its approved-edit round and the resumed execution boundary share. */
|
|
@@ -111,12 +110,13 @@ export declare function skillAllowDecision(hit: SkillGrantHit, updatedInput: unk
|
|
|
111
110
|
* releases anything — the entries it releases for the rest of the run in this mode (an entry that grants nothing, or
|
|
112
111
|
* stands down in this mode, is not named). Deployment text, sanitized and capped.
|
|
113
112
|
*/
|
|
114
|
-
export declare function skillActivationQuestion(skill: SkillGrantSource, mode: PermissionMode | undefined): string;
|
|
113
|
+
export declare function skillActivationQuestion(skill: SkillGrantSource, mode: PermissionMode | undefined, roster?: ReadonlySet<string>): string;
|
|
115
114
|
/** The face the gate and the resumed execution boundary read the table through, bound to the leg's call facts. */
|
|
116
115
|
export interface SkillGrantLane {
|
|
117
116
|
/** The allow layer's skill member: what grants this call in this mode, or undefined. */
|
|
118
117
|
allowing(req: ToolCallRequest, mode: PermissionMode | undefined): SkillGrantHit | undefined;
|
|
119
|
-
/**
|
|
118
|
+
/** What refuses this call, or undefined: the run's deny rows, else — for a call to the `Skill` tool — a named skill whose
|
|
119
|
+
* `disallowed-tools` holds an entry that reads as nothing (`readsAsNothing`). */
|
|
120
120
|
denying(req: ToolCallRequest): SkillGrantHit | undefined;
|
|
121
121
|
}
|
|
122
122
|
/** The gate inputs the run's skill rows ride (a member group of `ToolGateInput`). */
|
|
@@ -141,7 +141,7 @@ export interface SkillGateSeats {
|
|
|
141
141
|
/** The `Skill` tool's per-call seat over the loaded skills: a skill that declares a list states no permission of its own. */
|
|
142
142
|
export type SkillSeatReader = (args: unknown, mode: PermissionMode | undefined) => PermissionSeatReading | undefined;
|
|
143
143
|
/** The reader over these skills (`undefined` when none is loaded): `Execute skill: <name>` for a skill declaring a list. */
|
|
144
|
-
export declare function skillSeatReader(skills: readonly SkillGrantSource[]): SkillSeatReader | undefined;
|
|
144
|
+
export declare function skillSeatReader(skills: readonly SkillGrantSource[], mounted?: MountedToolNames): SkillSeatReader | undefined;
|
|
145
145
|
/**
|
|
146
146
|
* The gate's seat inputs for one call, composed in ONE place: the mounted instance's static seat (`calledName`), the `Skill`
|
|
147
147
|
* tool's per-call reader in the leg's live mode (only when the call's wire name is the engine's `Skill` — `skillToolName`),
|
|
@@ -162,7 +162,8 @@ export declare function skillGateWiring(opts: {
|
|
|
162
162
|
* parent's capture, held until this child ends, and a resumed leg's frozen rows, which keep the word `model` where the
|
|
163
163
|
* model wrote them) and the skills the person invoked by name (`user` — the invocation is the statement, so no card). Every invoked
|
|
164
164
|
* name must be one of the loaded skills; anything else (or a value that is not a list of names) refuses the run loudly
|
|
165
|
-
* (`config.user_invoked_skill_unknown`), never dropped
|
|
165
|
+
* (`config.user_invoked_skill_unknown`), never dropped; an invoked skill, or a carried deny record, whose deny entry reads as
|
|
166
|
+
* nothing refuses it too ({@link SKILL_DENY_UNREADABLE_CODE}). Returns the entries the grammar reads as nothing, across every
|
|
166
167
|
* loaded skill — the start-of-run notice's content.
|
|
167
168
|
*/
|
|
168
169
|
export declare function seedSkillGrants(table: SkillGrantTable, opts: {
|
|
@@ -170,23 +171,50 @@ export declare function seedSkillGrants(table: SkillGrantTable, opts: {
|
|
|
170
171
|
userInvokedSkills: unknown;
|
|
171
172
|
inherited: readonly SkillGrantRecord[] | undefined;
|
|
172
173
|
}): readonly SkillInertEntry[];
|
|
174
|
+
/**
|
|
175
|
+
* The user-invoked half of {@link seedSkillGrants}, read where no roster exists yet — a resume's pre-claim screen, over the
|
|
176
|
+
* re-supplied config: the invoked names seeded exactly as prepare seeds them, on a table told no mounted names, so only what
|
|
177
|
+
* every roster refuses alike refuses here (a name the config's skills do not carry; a deny entry the grammar alone reads as
|
|
178
|
+
* nothing). A name the leg's roster reads as stale or a near miss is prepare's to refuse; any other throw (a value prepare
|
|
179
|
+
* reports its own way) is left to prepare.
|
|
180
|
+
*/
|
|
181
|
+
export declare function screenUserInvokedSkills(config: {
|
|
182
|
+
readonly skills?: readonly SkillGrantSource[];
|
|
183
|
+
readonly userInvokedSkills?: unknown;
|
|
184
|
+
}): void;
|
|
173
185
|
/** The per-task table. Identity is the contract: the `Skill` tool, prepare's seeds, the tool ctx's snapshot and the
|
|
174
186
|
* lane all hold this one object. */
|
|
175
187
|
export declare class SkillGrantTable {
|
|
176
188
|
private rowsHeld;
|
|
189
|
+
private readonly rowKeys;
|
|
190
|
+
private mounted;
|
|
191
|
+
private activations;
|
|
192
|
+
/** State where the run's mounted names are read (prepare, once the roster is assembled): a skill's entries are read against
|
|
193
|
+
* them when seeded and whenever the skill is activated. A table never told reads the grammar alone. */
|
|
194
|
+
bindMountedNames(mounted: MountedToolNames): void;
|
|
195
|
+
/** State the skills the run's `Skill` tool activates (the list it serves) and its wire name, so a call naming one whose deny
|
|
196
|
+
* entries read as nothing is refused where the deny rows are read. A table never told refuses such a skill at activation only. */
|
|
197
|
+
bindSkills(skills: readonly SkillGrantSource[], skillToolName: string): void;
|
|
198
|
+
/** The names the run mounts as they stand, or undefined when none were stated. */
|
|
199
|
+
mountedNames(): ReadonlySet<string> | undefined;
|
|
177
200
|
/** The table never holds the same row twice (skill, origin, list, entry) — one rule for both writers: a re-activation, a
|
|
178
201
|
* capture re-supplied beside the frozen rows that already carry it. Keyed on the ROW, not the skill: a definition that
|
|
179
202
|
* changed across a park writes its new rows when it is activated again. */
|
|
180
203
|
private hold;
|
|
181
|
-
/** Write a skill's rows (rows already held are not held twice)
|
|
204
|
+
/** Write a skill's rows (rows already held are not held twice) — or none, refused with {@link SKILL_DENY_UNREADABLE_CODE},
|
|
205
|
+
* when its `disallowed-tools` holds an entry that reads as nothing. Returns the allow entries that read as nothing. */
|
|
182
206
|
activate(skill: SkillGrantSource, origin: SkillGrantOrigin): readonly SkillInertEntry[];
|
|
183
207
|
/** Seed portable records (a delegated child's capture, a resumed leg's frozen rows): a record the model wrote keeps that
|
|
184
|
-
* word, every other record seeds under `origin` — `user` is written only from this leg's own spec, never seeded.
|
|
208
|
+
* word, every other record seeds under `origin` — `user` is written only from this leg's own spec, never seeded. Each
|
|
209
|
+
* record is read as its writer read it, against the leg's roster: an allow record that reads as nothing grants nothing
|
|
210
|
+
* (not held); a deny record that does refuses the seed ({@link SKILL_DENY_UNREADABLE_CODE}) and nothing is held. */
|
|
185
211
|
seed(records: readonly SkillGrantRecord[], origin: SkillGrantOrigin): void;
|
|
186
212
|
/** The rows as portable records (a copy). */
|
|
187
213
|
records(): readonly SkillGrantRecord[];
|
|
188
214
|
/** The lane over this table, bound to the leg's call facts. */
|
|
189
215
|
lane(ctx: AllowEntryJudgeContext): SkillGrantLane;
|
|
216
|
+
/** A call to the `Skill` tool naming a skill whose deny entries read as nothing, read as the activation would read it. */
|
|
217
|
+
private activationRefused;
|
|
190
218
|
}
|
|
191
219
|
/** Read a delegated child's capture ({@link readCarriedRecords}): `undefined` refuses the delegation. */
|
|
192
220
|
export declare function readSkillGrantCapture(snapshot: readonly unknown[] | undefined): readonly SkillGrantRecord[] | undefined;
|
|
@@ -198,3 +226,6 @@ export declare function frozenSkillGrants(table: Pick<SkillGrantTable, "records"
|
|
|
198
226
|
export declare function readFrozenSkillGrants(section: unknown): readonly SkillGrantRecord[] | undefined;
|
|
199
227
|
/** The refusal for a `TaskSpec.userInvokedSkills` name the run does not carry (or a value that is not a list of names). */
|
|
200
228
|
export declare const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
|
|
229
|
+
/** The refusal for a skill whose `disallowed-tools` holds an entry that reads as nothing: the activation, or the leg that
|
|
230
|
+
* would carry the skill's deny record, does not happen. */
|
|
231
|
+
export declare const SKILL_DENY_UNREADABLE_CODE = "config.skill_deny_unreadable";
|
package/dist/core/skill-grant.js
CHANGED
|
@@ -3,25 +3,32 @@ import { parsePermissionRule } from "./permission-rules.js";
|
|
|
3
3
|
import { mountedToolPermissionSeat } from "./mode-policy.js";
|
|
4
4
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
5
5
|
import { boundInputHashOf } from "./canonical-json.js";
|
|
6
|
-
import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, judgeAllowEntries } from "./allow-entry.js";
|
|
6
|
+
import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, inertEntryPhrase, judgeAllowEntries } from "./allow-entry.js";
|
|
7
7
|
export const SKILL_GRANT_ORIGINS = Object.freeze(["model", "user", "inherited"]);
|
|
8
8
|
const FROZEN_SKILL_GRANT_ORIGINS = new Set(["model", "inherited"]);
|
|
9
9
|
export function skillNamedBy(args, skills) {
|
|
10
10
|
const named = args?.skill;
|
|
11
11
|
return typeof named === "string" ? skills.find((s) => s.name === named) : undefined;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
function skillGrantBinding(skill) {
|
|
14
14
|
const canonical = (entries) => [...new Set((entries ?? []).map((e) => {
|
|
15
15
|
const p = parsePermissionRule(e);
|
|
16
16
|
return p.ruleContent === undefined ? p.toolName : `${p.toolName}(${p.ruleContent})`;
|
|
17
17
|
}))].sort();
|
|
18
|
-
|
|
18
|
+
const boundGrants = { skill: skill?.name ?? null, allowedTools: canonical(skill?.allowedTools), disallowedTools: canonical(skill?.disallowedTools) };
|
|
19
|
+
return { boundGrantDigest: boundInputHashOf(boundGrants), boundGrants };
|
|
20
|
+
}
|
|
21
|
+
export function skillGrantDigest(skill) {
|
|
22
|
+
return skillGrantBinding(skill).boundGrantDigest;
|
|
19
23
|
}
|
|
20
|
-
export function
|
|
21
|
-
return
|
|
24
|
+
export function skillGrantBindingMembers(call, args, skills, skillToolName) {
|
|
25
|
+
return isSkillActivationCall(call, skills, skillToolName) ? skillGrantBinding(skillNamedBy(args, skills)) : {};
|
|
26
|
+
}
|
|
27
|
+
function isSkillActivationCall(call, skills, skillToolName) {
|
|
28
|
+
return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName;
|
|
22
29
|
}
|
|
23
30
|
export function parkedSkillApprovalRefusal(row, args, skills, skillToolName, site = "claim") {
|
|
24
|
-
if (row.boundGrantDigest === undefined &&
|
|
31
|
+
if (row.boundGrantDigest === undefined && skillGrantBindingMembers(row, args, skills, skillToolName).boundGrantDigest === undefined)
|
|
25
32
|
return undefined;
|
|
26
33
|
if (skillGrantDigest(skillNamedBy(args, skills)) === row.boundGrantDigest)
|
|
27
34
|
return undefined;
|
|
@@ -43,18 +50,18 @@ export function detachSkillLists(skills) {
|
|
|
43
50
|
export function declaresSkillLists(skill) {
|
|
44
51
|
return (skill.allowedTools?.length ?? 0) > 0 || (skill.disallowedTools?.length ?? 0) > 0;
|
|
45
52
|
}
|
|
46
|
-
export function compileSkillGrant(skill, origin) {
|
|
53
|
+
export function compileSkillGrant(skill, origin, roster) {
|
|
47
54
|
const rows = [];
|
|
48
55
|
const inert = [];
|
|
49
56
|
for (const [list, behavior, entries] of [
|
|
50
57
|
["allowed-tools", "allow", skill.allowedTools ?? []],
|
|
51
58
|
["disallowed-tools", "deny", skill.disallowedTools ?? []],
|
|
52
59
|
]) {
|
|
53
|
-
const compiled = compileAllowList(entries, behavior);
|
|
60
|
+
const compiled = compileAllowList(entries, behavior, roster);
|
|
54
61
|
for (const r of compiled.rows)
|
|
55
62
|
rows.push({ skill: skill.name, origin, behavior, entry: r.entry, compiled: r.compiled });
|
|
56
63
|
for (const i of compiled.inert)
|
|
57
|
-
inert.push({ skill: skill.name, list,
|
|
64
|
+
inert.push({ skill: skill.name, list, ...i });
|
|
58
65
|
}
|
|
59
66
|
return { rows, inert };
|
|
60
67
|
}
|
|
@@ -65,10 +72,15 @@ function judge(rows, req, behavior, ctx) {
|
|
|
65
72
|
export function skillDenyDecision(hit) {
|
|
66
73
|
return {
|
|
67
74
|
action: "deny",
|
|
68
|
-
message:
|
|
75
|
+
message: hit.readsAsNothing !== undefined
|
|
76
|
+
? skillDenyUnreadable(hit.skill, hit.readsAsNothing, "activation").message
|
|
77
|
+
: `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) — refused for the rest of this run`,
|
|
69
78
|
decisionReason: "skill",
|
|
70
79
|
};
|
|
71
80
|
}
|
|
81
|
+
function unreadDenyOf(inert) {
|
|
82
|
+
return inert.filter((i) => i.list === "disallowed-tools");
|
|
83
|
+
}
|
|
72
84
|
export function skillRowRefusal(lane, req) {
|
|
73
85
|
const hit = lane?.denying(req);
|
|
74
86
|
return hit === undefined ? undefined : skillDenyDecision(hit);
|
|
@@ -83,21 +95,21 @@ export function skillAllowDecision(hit, updatedInput) {
|
|
|
83
95
|
}
|
|
84
96
|
const CARD_ENTRIES_MAX = 10;
|
|
85
97
|
const CARD_ENTRY_MAX_CHARS = 120;
|
|
86
|
-
export function skillActivationQuestion(skill, mode) {
|
|
98
|
+
export function skillActivationQuestion(skill, mode, roster) {
|
|
87
99
|
const head = `Execute skill: ${inlineUntrusted(skill.name, 200)}`;
|
|
88
|
-
const granting = compileSkillGrant(skill, "model").rows.filter((r) => r.behavior === "allow" && !(mode === "auto" && autoStripped(r.compiled, undefined)));
|
|
100
|
+
const granting = compileSkillGrant(skill, "model", roster).rows.filter((r) => r.behavior === "allow" && !(mode === "auto" && autoStripped(r.compiled, undefined)));
|
|
89
101
|
if (granting.length === 0)
|
|
90
102
|
return head;
|
|
91
103
|
const shown = granting.slice(0, CARD_ENTRIES_MAX).map((r) => inlineUntrusted(r.entry, CARD_ENTRY_MAX_CHARS));
|
|
92
104
|
const rest = granting.length - shown.length;
|
|
93
105
|
return `${head}\nApproving also lets these tools be used without asking for the rest of this run: ${shown.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
|
|
94
106
|
}
|
|
95
|
-
export function skillSeatReader(skills) {
|
|
107
|
+
export function skillSeatReader(skills, mounted) {
|
|
96
108
|
if (skills.length === 0)
|
|
97
109
|
return undefined;
|
|
98
110
|
return (args, mode) => {
|
|
99
111
|
const skill = skillNamedBy(args, skills);
|
|
100
|
-
return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode) } : undefined;
|
|
112
|
+
return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode, mounted?.()) } : undefined;
|
|
101
113
|
};
|
|
102
114
|
}
|
|
103
115
|
export function skillGateWiring(opts) {
|
|
@@ -123,27 +135,68 @@ export function seedSkillGrants(table, opts) {
|
|
|
123
135
|
for (const name of invoked)
|
|
124
136
|
table.activate(named(name), "user");
|
|
125
137
|
}
|
|
126
|
-
|
|
138
|
+
const roster = table.mountedNames();
|
|
139
|
+
return opts.skills.flatMap((s) => compileSkillGrant(s, "model", roster).inert);
|
|
140
|
+
}
|
|
141
|
+
export function screenUserInvokedSkills(config) {
|
|
142
|
+
if (config.userInvokedSkills === undefined)
|
|
143
|
+
return;
|
|
144
|
+
try {
|
|
145
|
+
seedSkillGrants(new SkillGrantTable(), { skills: config.skills ?? [], userInvokedSkills: config.userInvokedSkills, inherited: [] });
|
|
146
|
+
}
|
|
147
|
+
catch (e) {
|
|
148
|
+
const code = e?.code;
|
|
149
|
+
if (code === USER_INVOKED_SKILL_UNKNOWN_CODE || code === SKILL_DENY_UNREADABLE_CODE)
|
|
150
|
+
throw e;
|
|
151
|
+
}
|
|
127
152
|
}
|
|
128
153
|
export class SkillGrantTable {
|
|
129
154
|
rowsHeld = [];
|
|
155
|
+
rowKeys = new Set();
|
|
156
|
+
mounted;
|
|
157
|
+
activations;
|
|
158
|
+
bindMountedNames(mounted) {
|
|
159
|
+
this.mounted = mounted;
|
|
160
|
+
}
|
|
161
|
+
bindSkills(skills, skillToolName) {
|
|
162
|
+
this.activations = { skills, skillToolName };
|
|
163
|
+
}
|
|
164
|
+
mountedNames() {
|
|
165
|
+
return this.mounted?.();
|
|
166
|
+
}
|
|
130
167
|
hold(row) {
|
|
131
|
-
|
|
132
|
-
|
|
168
|
+
const key = JSON.stringify([row.skill, row.origin, row.behavior, row.entry]);
|
|
169
|
+
if (this.rowKeys.has(key))
|
|
170
|
+
return;
|
|
171
|
+
this.rowKeys.add(key);
|
|
172
|
+
this.rowsHeld.push(row);
|
|
133
173
|
}
|
|
134
174
|
activate(skill, origin) {
|
|
135
|
-
const { rows, inert } = compileSkillGrant(skill, origin);
|
|
175
|
+
const { rows, inert } = compileSkillGrant(skill, origin, this.mountedNames());
|
|
176
|
+
const unreadDeny = unreadDenyOf(inert);
|
|
177
|
+
if (unreadDeny.length > 0)
|
|
178
|
+
throw skillDenyUnreadable(skill.name, unreadDeny, "activation");
|
|
136
179
|
for (const row of rows)
|
|
137
180
|
this.hold(row);
|
|
138
181
|
return inert;
|
|
139
182
|
}
|
|
140
183
|
seed(records, origin) {
|
|
184
|
+
const roster = this.mountedNames();
|
|
185
|
+
const rows = [];
|
|
186
|
+
const unreadDeny = [];
|
|
141
187
|
for (const r of records) {
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
188
|
+
const read = compileAllowList([r.entry], r.behavior, roster);
|
|
189
|
+
const row = read.rows[0];
|
|
190
|
+
if (row !== undefined)
|
|
191
|
+
rows.push({ skill: r.skill, origin: r.origin === "model" ? "model" : origin, behavior: r.behavior, entry: r.entry, compiled: row.compiled });
|
|
192
|
+
else if (r.behavior === "deny")
|
|
193
|
+
unreadDeny.push(...read.inert.map((i) => ({ skill: r.skill, ...i })));
|
|
146
194
|
}
|
|
195
|
+
const first = unreadDeny[0];
|
|
196
|
+
if (first !== undefined)
|
|
197
|
+
throw skillDenyUnreadable(first.skill, unreadDeny.filter((i) => i.skill === first.skill), "carried");
|
|
198
|
+
for (const row of rows)
|
|
199
|
+
this.hold(row);
|
|
147
200
|
}
|
|
148
201
|
records() {
|
|
149
202
|
return this.rowsHeld.map((r) => ({ skill: r.skill, behavior: r.behavior, entry: r.entry, origin: r.origin }));
|
|
@@ -151,9 +204,17 @@ export class SkillGrantTable {
|
|
|
151
204
|
lane(ctx) {
|
|
152
205
|
return {
|
|
153
206
|
allowing: (req, mode) => judge(allowingRows(this.rowsHeld, req, mode), req, "allow", ctx),
|
|
154
|
-
denying: (req) => judge(this.rowsHeld, req, "deny", ctx),
|
|
207
|
+
denying: (req) => judge(this.rowsHeld, req, "deny", ctx) ?? this.activationRefused(req),
|
|
155
208
|
};
|
|
156
209
|
}
|
|
210
|
+
activationRefused(req) {
|
|
211
|
+
const at = this.activations;
|
|
212
|
+
if (at === undefined || !isSkillActivationCall(req, at.skills, at.skillToolName))
|
|
213
|
+
return undefined;
|
|
214
|
+
const skill = skillNamedBy(req.args, at.skills);
|
|
215
|
+
const unread = skill === undefined ? [] : unreadDenyOf(compileSkillGrant(skill, "model", this.mountedNames()).inert);
|
|
216
|
+
return skill === undefined || unread.length === 0 ? undefined : { skill: skill.name, entries: unread.map((i) => i.entry), readsAsNothing: unread };
|
|
217
|
+
}
|
|
157
218
|
}
|
|
158
219
|
function readCarriedRecords(raw, carrier) {
|
|
159
220
|
if (raw === undefined)
|
|
@@ -190,3 +251,15 @@ export function readFrozenSkillGrants(section) {
|
|
|
190
251
|
return readCarriedRecords(section, "frozen");
|
|
191
252
|
}
|
|
192
253
|
export const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
|
|
254
|
+
export const SKILL_DENY_UNREADABLE_CODE = "config.skill_deny_unreadable";
|
|
255
|
+
function skillDenyUnreadable(skill, entries, site) {
|
|
256
|
+
const listed = entries.map(inertEntryPhrase).join(", ");
|
|
257
|
+
const one = entries.length === 1;
|
|
258
|
+
const name = inlineUntrusted(skill, 120);
|
|
259
|
+
const message = site === "activation"
|
|
260
|
+
? `Skill "${name}" was not activated: its disallowed-tools entr${one ? `y ${listed} reads` : `ies ${listed} read`} as nothing — a refusal this engine cannot read is not dropped, so the activation does not happen (its instructions are not loaded and nothing it lists is granted). Spell each entry as the tool is mounted, or remove it, in its definition.`
|
|
261
|
+
: `the grant rows carried for Skill "${name}" (a park's frozen rows, or a parent's capture) hold the disallowed-tools entr${one ? `y ${listed}, which reads` : `ies ${listed}, which read`} as nothing for this run — a refusal this engine cannot read is not dropped, so this leg does not start without it. A parked task an earlier release wrote can be finished on a worker of that release; otherwise correct the entry in its disallowed-tools and submit the task again.`;
|
|
262
|
+
const e = new Error(message);
|
|
263
|
+
e.code = SKILL_DENY_UNREADABLE_CODE;
|
|
264
|
+
return e;
|
|
265
|
+
}
|