@lmzhen/dsh-evolution-core 0.26.15 → 0.27.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/README.md +16 -1
- package/lib/composition/index.js +3 -0
- package/lib/composition-D8Y3-ROd.js +604 -0
- package/lib/ensure-MEYMj9jn.js +222 -0
- package/lib/errors-Br2-uUSK.js +57 -0
- package/lib/http-routes-BP-S3KV-.js +149 -0
- package/lib/http-routes.js +2 -0
- package/lib/index.js +777 -840
- package/lib/restrictor.js +37 -0
- package/lib/subagent/index.js +186 -0
- package/lib/types/citations.js +192 -0
- package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +2 -2
- package/lib/types/composition/composer.js +681 -0
- package/lib/types/composition/ensure.d.ts +10 -0
- package/lib/types/composition/ensure.js +95 -0
- package/lib/types/composition/index.d.ts +5 -0
- package/lib/types/composition/index.js +7 -0
- package/lib/types/composition/preset-ids.d.ts +17 -0
- package/lib/types/composition/preset-ids.js +21 -0
- package/lib/types/composition/restricted-preset.d.ts +36 -0
- package/lib/types/composition/restricted-preset.js +42 -0
- package/lib/types/constants.js +223 -0
- package/lib/types/cost.js +67 -0
- package/lib/types/curator.d.ts +2 -0
- package/lib/types/curator.js +346 -0
- package/lib/types/diagnostics.d.ts +26 -0
- package/lib/types/diagnostics.js +23 -0
- package/lib/types/document-body.js +91 -0
- package/lib/types/drift-signals.js +319 -0
- package/lib/types/env.js +25 -0
- package/lib/types/errors.d.ts +4 -1
- package/lib/types/errors.js +59 -0
- package/lib/types/events.js +16 -0
- package/lib/types/evidence.js +65 -0
- package/lib/types/evolution-events.js +479 -0
- package/lib/types/fold-untrusted.js +44 -0
- package/lib/types/frontmatter.js +623 -0
- package/lib/types/fuzzy-match.js +135 -0
- package/lib/types/gates.js +51 -0
- package/lib/types/guards.js +27 -0
- package/lib/types/health-baseline.js +63 -0
- package/lib/types/http-routes.d.ts +67 -35
- package/lib/types/http-routes.js +135 -0
- package/lib/types/index.d.ts +13 -2
- package/lib/types/index.js +101 -0
- package/lib/types/instance-scope.js +69 -0
- package/lib/types/io.js +675 -0
- package/lib/types/learn-prompt.js +42 -0
- package/lib/types/limits.js +63 -0
- package/lib/types/live.js +107 -0
- package/lib/types/long-runs.js +96 -0
- package/lib/types/memory-store.d.ts +0 -16
- package/lib/types/memory-store.js +606 -0
- package/lib/types/mutations.js +95 -0
- package/lib/types/namespace-migration.js +156 -0
- package/lib/types/numeric.js +47 -0
- package/lib/types/opt-in.js +101 -0
- package/lib/types/package-identity.d.ts +14 -0
- package/lib/types/package-identity.js +47 -0
- package/lib/types/params.js +328 -0
- package/lib/types/payload.js +36 -0
- package/lib/types/platform-line.generated.d.ts +10 -0
- package/lib/types/platform-line.generated.js +12 -0
- package/lib/types/platform-services.d.ts +2 -2
- package/lib/types/platform-services.js +52 -0
- package/lib/types/probe.d.ts +9 -3
- package/lib/types/probe.js +122 -0
- package/lib/types/prompts.js +409 -0
- package/lib/types/quality.js +308 -0
- package/lib/types/reachability.d.ts +46 -0
- package/lib/types/reachability.js +122 -0
- package/lib/types/redact.js +257 -0
- package/lib/types/reference-rewrite.js +144 -0
- package/lib/types/reports.js +110 -0
- package/lib/types/restrictor.d.ts +11 -0
- package/lib/types/restrictor.js +48 -0
- package/lib/types/review-channel.js +59 -0
- package/lib/types/run-registry.js +345 -0
- package/lib/types/scope.js +7 -0
- package/lib/types/serial.js +17 -0
- package/lib/types/session-fold.d.ts +15 -0
- package/lib/types/session-fold.js +28 -0
- package/lib/types/session-projection.js +295 -0
- package/lib/types/signals.js +209 -0
- package/lib/types/skill-discovery.js +66 -0
- package/lib/types/skill-health.js +134 -0
- package/lib/types/skill-history.js +745 -0
- package/lib/types/skill-lint.js +112 -0
- package/lib/types/skill-reads.js +67 -0
- package/lib/types/skill-row-cells.js +123 -0
- package/lib/types/skill-store.d.ts +34 -3
- package/lib/types/skill-store.js +3528 -0
- package/lib/types/state-store.js +60 -0
- package/lib/types/store-ledger.d.ts +44 -0
- package/lib/types/store-ledger.js +117 -0
- package/lib/types/subagent/channel.d.ts +4 -0
- package/lib/types/subagent/channel.js +131 -0
- package/lib/types/subagent/index.d.ts +4 -0
- package/lib/types/subagent/index.js +5 -0
- package/lib/types/subagent/readiness.d.ts +6 -0
- package/lib/types/subagent/readiness.js +35 -0
- package/lib/types/subagent/types.d.ts +58 -0
- package/lib/types/subagent/types.js +2 -0
- package/lib/types/threats.js +338 -0
- package/lib/types/tool-dispatch.d.ts +5 -0
- package/lib/types/tool-dispatch.js +535 -0
- package/lib/types/tree-write.d.ts +53 -0
- package/lib/types/tree-write.js +40 -0
- package/lib/types/unit-members.js +121 -0
- package/lib/types/usage.js +474 -0
- package/lib/types/write-authority.d.ts +87 -0
- package/lib/types/write-authority.js +23 -0
- package/lib/types/write-gates.d.ts +111 -0
- package/lib/types/write-gates.js +344 -0
- package/lib/types/write-inventory.js +158 -0
- package/package.json +39 -11
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* G1/1.1: the ONE authority behind a write.
|
|
3
|
+
*
|
|
4
|
+
* Before this type existed the inputs of the can-I-write decision came from two places with two
|
|
5
|
+
* lifecycles: `origin` (plus the policy snapshot and the confirm seam) travelled in the tool
|
|
6
|
+
* package gate context, while `threatExemptLabels` rode the SkillLibrary constructor. Callers that
|
|
7
|
+
* reached the bytes through another path built neither, and the mutators defaulted `origin` to
|
|
8
|
+
* foreground - so an omitted authority silently read as the operator acting, and no mechanism said
|
|
9
|
+
* otherwise (measured: two entry points in the same curator file, one passing background_review and
|
|
10
|
+
* one defaulting).
|
|
11
|
+
*
|
|
12
|
+
* The shape below is where those inputs converge. The follow-up batch makes it a REQUIRED argument
|
|
13
|
+
* of every mutator (content and lifecycle alike) and deletes the defaults, so an omission is a
|
|
14
|
+
* compile error instead of a silent claim about who wrote the bytes.
|
|
15
|
+
*/
|
|
16
|
+
import type { WriteOrigin } from './skill-store.ts';
|
|
17
|
+
/**
|
|
18
|
+
* The confirmation contract, HOISTED here from tool-skill-manage (G1/1.1 follow-up): the authority
|
|
19
|
+
* carries the confirm seam, so the seam's vocabulary lives with it. tool-skill-manage re-exports
|
|
20
|
+
* these names from write-gates.ts unchanged, so nothing importing them from there moves.
|
|
21
|
+
*/
|
|
22
|
+
export type WriteConfirmMode = 'auto' | 'ask' | 'timeout';
|
|
23
|
+
/** The mode a deployment that configures nothing gets. */
|
|
24
|
+
export declare const DEFAULT_WRITE_CONFIRM_MODE: WriteConfirmMode;
|
|
25
|
+
/** Seconds a timeout-mode prompt waits before the write is cancelled. */
|
|
26
|
+
export declare const DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS = 120;
|
|
27
|
+
/** What the operator is told about the target before answering. */
|
|
28
|
+
export interface ConfirmContext {
|
|
29
|
+
readonly description?: string | undefined;
|
|
30
|
+
readonly versions?: number | undefined;
|
|
31
|
+
}
|
|
32
|
+
/** One confirm question, as the gate writes it and the seam asks it. */
|
|
33
|
+
export interface WriteConfirmRequest {
|
|
34
|
+
readonly action: string;
|
|
35
|
+
readonly name: string;
|
|
36
|
+
readonly question: {
|
|
37
|
+
readonly id: string;
|
|
38
|
+
readonly header: string;
|
|
39
|
+
readonly question: string;
|
|
40
|
+
readonly options: readonly {
|
|
41
|
+
readonly label: string;
|
|
42
|
+
}[];
|
|
43
|
+
};
|
|
44
|
+
readonly confirmLabel: string;
|
|
45
|
+
readonly signal?: AbortSignal;
|
|
46
|
+
}
|
|
47
|
+
/** Ask the human to confirm one irreversible skill write. MUST be total: every unavailability
|
|
48
|
+
* resolves to true, so no failure of the question service blocks the operator's own write. */
|
|
49
|
+
export type WriteConfirm = (request: WriteConfirmRequest) => Promise<boolean>;
|
|
50
|
+
/**
|
|
51
|
+
* The batch-one migration aid: every call site becomes ONE of these instead of hand-building the
|
|
52
|
+
* object - so the shape cannot drift between callers, and each site still has to say its own
|
|
53
|
+
* origin out loud (origin is required here on purpose: the whole point is that no caller inherits
|
|
54
|
+
* a default identity).
|
|
55
|
+
*/
|
|
56
|
+
export declare function authorityFrom(input: {
|
|
57
|
+
origin: WriteOrigin;
|
|
58
|
+
threatExemptLabels?: readonly string[];
|
|
59
|
+
protectedNames?: readonly string[] | undefined;
|
|
60
|
+
readNames?: ReadonlySet<string> | undefined;
|
|
61
|
+
confirm?: WriteConfirm;
|
|
62
|
+
confirmMode?: WriteConfirmMode;
|
|
63
|
+
confirmTimeoutSeconds?: number;
|
|
64
|
+
/** Optional: a call site that has nothing to report may leave it out, and the gates then stay
|
|
65
|
+
* silent instead of forcing every one of the twelve construction points to invent a logger. */
|
|
66
|
+
warn?: (message: string) => void;
|
|
67
|
+
}): WriteAuthority;
|
|
68
|
+
export interface WriteAuthority {
|
|
69
|
+
/** Who is asking: the operator own session, a background review, the curator, the graph face. */
|
|
70
|
+
readonly origin: WriteOrigin;
|
|
71
|
+
/** Labels the threat scan must not refuse (P2-18). Migrated here from the library constructor:
|
|
72
|
+
* one decision must not read its policy from two objects. */
|
|
73
|
+
readonly threatExemptLabels: readonly string[];
|
|
74
|
+
/** The policy snapshot protected list; undefined when no policy row is mounted. */
|
|
75
|
+
readonly protectedNames: readonly string[] | undefined;
|
|
76
|
+
/** Names this session read successfully; undefined when the session log is not readable. */
|
|
77
|
+
readonly readNames: ReadonlySet<string> | undefined;
|
|
78
|
+
/** The human confirm seam, read only where a question is actually asked. */
|
|
79
|
+
readonly confirm?: WriteConfirm;
|
|
80
|
+
/** skillWriteConfirm: auto (the default) skips the question entirely. */
|
|
81
|
+
readonly confirmMode?: WriteConfirmMode;
|
|
82
|
+
/** skillWriteConfirmTimeoutSeconds, read only in timeout mode. */
|
|
83
|
+
readonly confirmTimeoutSeconds?: number;
|
|
84
|
+
/** Report a degraded gate; must not throw. */
|
|
85
|
+
readonly warn: (message: string) => void;
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=write-authority.d.ts.map
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The mode a deployment that configures nothing gets. */
|
|
2
|
+
export const DEFAULT_WRITE_CONFIRM_MODE = 'auto';
|
|
3
|
+
/** Seconds a timeout-mode prompt waits before the write is cancelled. */
|
|
4
|
+
export const DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS = 120;
|
|
5
|
+
/**
|
|
6
|
+
* The batch-one migration aid: every call site becomes ONE of these instead of hand-building the
|
|
7
|
+
* object - so the shape cannot drift between callers, and each site still has to say its own
|
|
8
|
+
* origin out loud (origin is required here on purpose: the whole point is that no caller inherits
|
|
9
|
+
* a default identity).
|
|
10
|
+
*/
|
|
11
|
+
export function authorityFrom(input) {
|
|
12
|
+
return {
|
|
13
|
+
origin: input.origin,
|
|
14
|
+
threatExemptLabels: input.threatExemptLabels ?? [],
|
|
15
|
+
protectedNames: input.protectedNames,
|
|
16
|
+
readNames: input.readNames,
|
|
17
|
+
...input.confirm === undefined ? {} : { confirm: input.confirm },
|
|
18
|
+
...input.confirmMode === undefined ? {} : { confirmMode: input.confirmMode },
|
|
19
|
+
...input.confirmTimeoutSeconds === undefined ? {} : { confirmTimeoutSeconds: input.confirmTimeoutSeconds },
|
|
20
|
+
warn: input.warn ?? (() => { }),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=write-authority.js.map
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write-admission sequence: ONE ordered table of the reasons a skill write is refused.
|
|
3
|
+
*
|
|
4
|
+
* Two points read this table (design `dsh-evolution-write-gate-design.md` §3). The ADMISSION
|
|
5
|
+
* point (`execute`, BEFORE the approval seam) runs every gate that applies to `'admission'`, so a
|
|
6
|
+
* write that is refused is never staged and never spends the operator's attention twice. The
|
|
7
|
+
* EXECUTION point (the `executeCore` funnel, reached by the direct path and by the approval
|
|
8
|
+
* replay) runs the gates that apply to `'execution'`: the stored plan of a replayed write passed
|
|
9
|
+
* no schema, and the protected list can change while a record sits pending, so those verdicts are
|
|
10
|
+
* re-decided at the bytes.
|
|
11
|
+
*
|
|
12
|
+
* Order is part of the contract: a gate that can only refuse comes before one that needs a round
|
|
13
|
+
* trip with the operator, so a write that is refused anyway never asks anything.
|
|
14
|
+
*
|
|
15
|
+
* A gate decides whether a write may be ATTEMPTED. The library decides whether it can be
|
|
16
|
+
* PERFORMED — its own structured refusals (a restructure element that is not an object, a patch
|
|
17
|
+
* anchor that no longer matches, an `absorbed_into` that does not exist) stay where the bytes are
|
|
18
|
+
* read.
|
|
19
|
+
*
|
|
20
|
+
* Rule bodies live in `evolution-core` (the read-before-write rule in `skill-reads`, the
|
|
21
|
+
* required-argument table in `SKILL_ACTION_REQUIRED_FIELDS`); this module owns the ORDER and the
|
|
22
|
+
* applicability of each gate, which is an execution-point fact of this tool.
|
|
23
|
+
* @module @lmzhen/dsh-evolution-core/write-gates
|
|
24
|
+
*/
|
|
25
|
+
import { type WriteConfirm, type WriteConfirmMode } from './write-authority.ts';
|
|
26
|
+
import type { ConfirmContext } from './write-authority.ts';
|
|
27
|
+
import { type WriteOrigin } from './index.ts';
|
|
28
|
+
/** The point whose verdict is being taken. */
|
|
29
|
+
export type WriteGatePoint = 'admission' | 'execution';
|
|
30
|
+
/** What every gate reads. */
|
|
31
|
+
/**
|
|
32
|
+
* G1/1.2: the policy snapshot's protected list, read through an `unknown` boundary.
|
|
33
|
+
*
|
|
34
|
+
* This used to be package-private to tool-skill-manage, which is why the graph commands could not ask
|
|
35
|
+
* the same question and ended up enforcing nothing. It lives here so every write entry point reads the
|
|
36
|
+
* SAME list from the SAME place; the `unknown` boundary stays because the Context augmentation types the
|
|
37
|
+
* getter non-optionally while at runtime the policy row may be absent.
|
|
38
|
+
* @param policy - `ctx.get('evolutionPolicy')`, or anything else when the row is not mounted.
|
|
39
|
+
* @returns the protected names, or undefined when no policy snapshot is readable.
|
|
40
|
+
*/
|
|
41
|
+
/**
|
|
42
|
+
* G1/1.2: what a write entry point must supply for the shared admission sequence.
|
|
43
|
+
*
|
|
44
|
+
* The gates need policy, confirm, the session's read set and a warn sink; every caller can obtain those
|
|
45
|
+
* from its own `ctx`, but assembling them into a valid `WriteGateContext` is the seam's business, not each
|
|
46
|
+
* caller's. Without this, "all five entry points call the same entry" means five hand-built contexts and
|
|
47
|
+
* five chances to leave `protectedNames` out - which is exactly how the graph commands ended up enforcing
|
|
48
|
+
* nothing.
|
|
49
|
+
*/
|
|
50
|
+
export interface SkillWriteGateServices {
|
|
51
|
+
/** `ctx.get('evolutionPolicy')`, or anything else when the row is not mounted. */
|
|
52
|
+
readonly policy: unknown;
|
|
53
|
+
/** The human confirm seam; `undefined` refuses to ask (the gates then refuse the write). */
|
|
54
|
+
readonly confirm: WriteConfirm | undefined;
|
|
55
|
+
/** Names this session read successfully; `undefined` when the session log is not readable. */
|
|
56
|
+
readonly readNames: ReadonlySet<string> | undefined;
|
|
57
|
+
/** Report a degraded gate; must not throw. */
|
|
58
|
+
readonly warn: (message: string) => void;
|
|
59
|
+
/** The registry's `skillWriteConfirm`; `auto` (the default) skips the question. */
|
|
60
|
+
readonly confirmMode?: WriteConfirmMode;
|
|
61
|
+
/** The registry's `skillWriteConfirmTimeoutSeconds`, read only in `timeout` mode. */
|
|
62
|
+
readonly confirmTimeoutSeconds?: number;
|
|
63
|
+
/** Lazily read the target's own words for the confirmation card. */
|
|
64
|
+
readonly describeTarget?: (() => Promise<ConfirmContext | undefined>) | undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Assemble a gate context from a caller's services. The protected list is read HERE, so no caller can
|
|
68
|
+
* forget it or read it from a second place.
|
|
69
|
+
* @param services - the four required services plus the optional registry knobs.
|
|
70
|
+
* @param request - the point asking, the arguments under test, and the write origin.
|
|
71
|
+
* @returns a context the shared sequence accepts.
|
|
72
|
+
*/
|
|
73
|
+
export declare function skillWriteGateContext(services: SkillWriteGateServices, request: {
|
|
74
|
+
readonly point: WriteGatePoint;
|
|
75
|
+
readonly args: unknown;
|
|
76
|
+
readonly origin: WriteOrigin;
|
|
77
|
+
}): WriteGateContext;
|
|
78
|
+
export declare function protectedSkillNamesOf(policy: unknown): readonly string[] | undefined;
|
|
79
|
+
export interface WriteGateContext {
|
|
80
|
+
/** The point asking: the tool's admission of a call, or its execution at the bytes. */
|
|
81
|
+
readonly point: WriteGatePoint;
|
|
82
|
+
/** The parsed (admission) or stored (replay) arguments — unvalidated, hence the shape gate. */
|
|
83
|
+
readonly args: unknown;
|
|
84
|
+
/** The library write origin: `foreground` is the operator's own session. */
|
|
85
|
+
readonly origin: WriteOrigin;
|
|
86
|
+
/** The policy snapshot's protected list; `undefined` when no policy row is mounted. */
|
|
87
|
+
readonly protectedNames: readonly string[] | undefined;
|
|
88
|
+
/** Names this session read successfully; `undefined` when the session log is not readable. */
|
|
89
|
+
readonly readNames: ReadonlySet<string> | undefined;
|
|
90
|
+
/** The human confirm seam — read only by the gates that apply to `'admission'`. */
|
|
91
|
+
readonly confirm: WriteConfirm | undefined;
|
|
92
|
+
/** The registry's `skillWriteConfirm`: `auto` (the default) skips the question entirely. */
|
|
93
|
+
readonly confirmMode?: WriteConfirmMode;
|
|
94
|
+
/** The registry's `skillWriteConfirmTimeoutSeconds`, read only in `timeout` mode. */
|
|
95
|
+
readonly confirmTimeoutSeconds?: number;
|
|
96
|
+
/** Read the target's own words for the confirmation card. LAZY on purpose: a write that is not
|
|
97
|
+
* asked about must not pay a read for a question nobody sees. Absent means "say nothing extra". */
|
|
98
|
+
readonly describeTarget?: (() => Promise<ConfirmContext | undefined>) | undefined;
|
|
99
|
+
/** Report a degraded gate; must not throw. The implementation decides how often it speaks — the
|
|
100
|
+
* shipped seam latches once per PROCESS, because the conditions it reports (no question service,
|
|
101
|
+
* an unreadable session log) belong to the deployment, not to one write. */
|
|
102
|
+
readonly warn: (message: string) => void;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Run the sequence for one write attempt.
|
|
106
|
+
* @param context - the asking point, the write, and the seams the human-facing gate needs.
|
|
107
|
+
* @returns the FIRST refusal, or null when every applicable gate passed. Both points return the
|
|
108
|
+
* message as the tool result unchanged, so the two entries refuse with identical wording.
|
|
109
|
+
*/
|
|
110
|
+
export declare function runWriteGates(context: WriteGateContext): Promise<string | null>;
|
|
111
|
+
//# sourceMappingURL=write-gates.d.ts.map
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write-admission sequence: ONE ordered table of the reasons a skill write is refused.
|
|
3
|
+
*
|
|
4
|
+
* Two points read this table (design `dsh-evolution-write-gate-design.md` §3). The ADMISSION
|
|
5
|
+
* point (`execute`, BEFORE the approval seam) runs every gate that applies to `'admission'`, so a
|
|
6
|
+
* write that is refused is never staged and never spends the operator's attention twice. The
|
|
7
|
+
* EXECUTION point (the `executeCore` funnel, reached by the direct path and by the approval
|
|
8
|
+
* replay) runs the gates that apply to `'execution'`: the stored plan of a replayed write passed
|
|
9
|
+
* no schema, and the protected list can change while a record sits pending, so those verdicts are
|
|
10
|
+
* re-decided at the bytes.
|
|
11
|
+
*
|
|
12
|
+
* Order is part of the contract: a gate that can only refuse comes before one that needs a round
|
|
13
|
+
* trip with the operator, so a write that is refused anyway never asks anything.
|
|
14
|
+
*
|
|
15
|
+
* A gate decides whether a write may be ATTEMPTED. The library decides whether it can be
|
|
16
|
+
* PERFORMED — its own structured refusals (a restructure element that is not an object, a patch
|
|
17
|
+
* anchor that no longer matches, an `absorbed_into` that does not exist) stay where the bytes are
|
|
18
|
+
* read.
|
|
19
|
+
*
|
|
20
|
+
* Rule bodies live in `evolution-core` (the read-before-write rule in `skill-reads`, the
|
|
21
|
+
* required-argument table in `SKILL_ACTION_REQUIRED_FIELDS`); this module owns the ORDER and the
|
|
22
|
+
* applicability of each gate, which is an execution-point fact of this tool.
|
|
23
|
+
* @module @lmzhen/dsh-evolution-core/write-gates
|
|
24
|
+
*/
|
|
25
|
+
import { DEFAULT_WRITE_CONFIRM_MODE, DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS } from "./write-authority.js";
|
|
26
|
+
import { SKILL_ACTION_REQUIRED_FIELDS, errorText, isUnreadWrite } from "./index.js";
|
|
27
|
+
/** The confirm question's stable id — one question per write, so an answer to an earlier prompt
|
|
28
|
+
* can never be read as the answer to a later one. */
|
|
29
|
+
const CONFIRM_QUESTION_ID = 'evolution-skill-write';
|
|
30
|
+
/** The cancel label every confirm question offers. */
|
|
31
|
+
const CANCEL_LABEL = '取消';
|
|
32
|
+
/** What a `timeout`-mode deadline settles with: comparable by identity against the seam's
|
|
33
|
+
* boolean, so the two outcomes stay distinguishable without a mutable flag (which a linter
|
|
34
|
+
* cannot follow through a timer callback, and a reader cannot either). */
|
|
35
|
+
const CONFIRM_TIMED_OUT = 'confirm-timed-out';
|
|
36
|
+
/**
|
|
37
|
+
* How the ONE confirmation behaves (registry `skillWriteConfirm`).
|
|
38
|
+
*
|
|
39
|
+
* `auto` writes without asking — the default, because an unattended run (a background pass, a
|
|
40
|
+
* scheduled review, a headless session) must not park a tool call on a question nobody will answer.
|
|
41
|
+
* `ask` waits for as long as it takes, which is the deployment that wants the gate in the loop.
|
|
42
|
+
* `timeout` asks and cancels the write on its own deadline.
|
|
43
|
+
*/
|
|
44
|
+
// G1/1.1: the confirm vocabulary lives in write-authority.ts with the authority that carries it.
|
|
45
|
+
// D-09 follow-up: this file used to RE-EXPORT it for compatibility, which made index.ts export the same
|
|
46
|
+
// name from two modules (TS2308: WriteConfirmMode/WriteConfirmRequest/DEFAULT_WRITE_CONFIRM_MODE).
|
|
47
|
+
// The names stay reachable through the package root via write-authority - one owner, no second export.
|
|
48
|
+
/** A usable deadline: the configured value when it is a positive finite number, else the default. */
|
|
49
|
+
function timeoutSecondsOf(configured) {
|
|
50
|
+
return configured !== undefined && Number.isFinite(configured) && configured >= 1
|
|
51
|
+
? Math.floor(configured)
|
|
52
|
+
: DEFAULT_WRITE_CONFIRM_TIMEOUT_SECONDS;
|
|
53
|
+
}
|
|
54
|
+
/** The scalar fields the tool schema types as strings; the replay channel has no schema.
|
|
55
|
+
* `action` is one of them: a non-string action used to fall through to the library as "Unknown
|
|
56
|
+
* action" while the read gate re-defaulted it to `patch` and refused with E-318 (review
|
|
57
|
+
* 2026-09-26, P2), so the shape gate refuses it by name instead. */
|
|
58
|
+
const SCALAR_ARG_FIELDS = ['action', 'name', 'content', 'old_string', 'new_string', 'file_path', 'file_content', 'absorbed_into'];
|
|
59
|
+
/** The scalar fields the tool schema types as BOOLEANS; the replay channel has no schema either.
|
|
60
|
+
* T2-V3/A28: `args.replace_all === true` answered "false" for anything else, so a staged
|
|
61
|
+
* `replace_all: 'yes'` silently patched only the FIRST occurrence — a wrong count with no signal.
|
|
62
|
+
* The plan channel already refuses a non-boolean `replace_all` by name (S1-C1); this is the same
|
|
63
|
+
* rule on the replay channel, so the two channels cannot disagree about one flag. */
|
|
64
|
+
const FLAG_ARG_FIELDS = ['replace_all'];
|
|
65
|
+
/**
|
|
66
|
+
* Assemble a gate context from a caller's services. The protected list is read HERE, so no caller can
|
|
67
|
+
* forget it or read it from a second place.
|
|
68
|
+
* @param services - the four required services plus the optional registry knobs.
|
|
69
|
+
* @param request - the point asking, the arguments under test, and the write origin.
|
|
70
|
+
* @returns a context the shared sequence accepts.
|
|
71
|
+
*/
|
|
72
|
+
export function skillWriteGateContext(services, request) {
|
|
73
|
+
return {
|
|
74
|
+
point: request.point,
|
|
75
|
+
args: request.args,
|
|
76
|
+
origin: request.origin,
|
|
77
|
+
protectedNames: protectedSkillNamesOf(services.policy),
|
|
78
|
+
readNames: services.readNames,
|
|
79
|
+
confirm: services.confirm,
|
|
80
|
+
...(services.confirmMode === undefined ? {} : { confirmMode: services.confirmMode }),
|
|
81
|
+
...(services.confirmTimeoutSeconds === undefined ? {} : { confirmTimeoutSeconds: services.confirmTimeoutSeconds }),
|
|
82
|
+
...(services.describeTarget === undefined ? {} : { describeTarget: services.describeTarget }),
|
|
83
|
+
warn: services.warn,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function protectedSkillNamesOf(policy) {
|
|
87
|
+
const snapshot = policy?.get?.();
|
|
88
|
+
return snapshot?.protectedSkillNames;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Narrow the arguments once for every gate.
|
|
92
|
+
*
|
|
93
|
+
* The admission point reaches the gates AFTER the schema, the replay channel BEFORE any
|
|
94
|
+
* validation, so a gate reads only what it can prove: anything else counts as absent, exactly as
|
|
95
|
+
* the missing-argument check already treated a non-string action.
|
|
96
|
+
* @param args - the arguments as the caller sent them.
|
|
97
|
+
* @returns the narrowed view.
|
|
98
|
+
*/
|
|
99
|
+
function gateArgsOf(args) {
|
|
100
|
+
const scalars = (typeof args === 'object' && args !== null ? args : {});
|
|
101
|
+
const action = scalars.action;
|
|
102
|
+
const name = scalars.name;
|
|
103
|
+
const absorbedInto = scalars.absorbed_into;
|
|
104
|
+
return {
|
|
105
|
+
action: typeof action === 'string' ? action : undefined,
|
|
106
|
+
name: typeof name === 'string' ? name : '',
|
|
107
|
+
// A BLANK absorbed_into is absent: the plan validator reads it the same way
|
|
108
|
+
// (`!(op.absorbed_into ?? '').trim()`), and the archive call itself is truthiness-based
|
|
109
|
+
// (`args.absorbed_into ? { absorbedInto } : {}`) — so `''` performs a BARE archive and must not
|
|
110
|
+
// borrow the merge exemption (review 2026-09-26, P1).
|
|
111
|
+
absorbedInto: typeof absorbedInto === 'string' && absorbedInto.trim() !== '' ? absorbedInto : undefined,
|
|
112
|
+
scalars,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The scalar arguments must be strings before any other gate reads them as names.
|
|
117
|
+
*
|
|
118
|
+
* v20 (D-1, V8-09 sibling): the schema does not strictly guarantee scalar shapes (the F-07 class
|
|
119
|
+
* of garbage that slipped past it), and a non-string scalar used to escape as a bare TypeError
|
|
120
|
+
* from SkillLibrary (`name.trim()` / `md.includes(...)`). Family posture: a STRUCTURED refusal.
|
|
121
|
+
* Absent is legal — the branches that need a value have their own remedies.
|
|
122
|
+
*/
|
|
123
|
+
const ARGUMENT_SHAPE = {
|
|
124
|
+
id: 'argument-shape',
|
|
125
|
+
appliesTo: ['admission', 'execution'],
|
|
126
|
+
run: ({ view }) => {
|
|
127
|
+
for (const field of SCALAR_ARG_FIELDS) {
|
|
128
|
+
const value = view.scalars[field];
|
|
129
|
+
if (value === undefined || value === null || typeof value === 'string')
|
|
130
|
+
continue;
|
|
131
|
+
return `skill_manage: "${field}" must be a string (got ${typeof value}); refusing the write.`;
|
|
132
|
+
}
|
|
133
|
+
// T2-V3/A28: the FLAGS are shapes too. A non-boolean `replace_all` used to mean "false" at the
|
|
134
|
+
// patch site, so the write landed with a different count than the caller asked for.
|
|
135
|
+
for (const field of FLAG_ARG_FIELDS) {
|
|
136
|
+
const value = view.scalars[field];
|
|
137
|
+
if (value === undefined || value === null || typeof value === 'boolean')
|
|
138
|
+
continue;
|
|
139
|
+
return `skill_manage: "${field}" must be a boolean (got ${typeof value}); refusing the write.`;
|
|
140
|
+
}
|
|
141
|
+
return null;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* V27 G5.1 (v27 T-2) / OPT-05 (2026-09): name the missing per-action arguments before anything is
|
|
146
|
+
* read or written. The table comes from core (the plan validator reads the same rows), and the
|
|
147
|
+
* OWN-property narrowing is required because a plain-object index resolves inherited members
|
|
148
|
+
* (`constructor`, `toString`, …) to truthy functions. An EMPTY string is deliberately NOT
|
|
149
|
+
* missing: it reaches the library, whose branch messages carry the more specific remedy.
|
|
150
|
+
*/
|
|
151
|
+
const MISSING_ARGS = {
|
|
152
|
+
id: 'missing-args',
|
|
153
|
+
appliesTo: ['admission', 'execution'],
|
|
154
|
+
run: ({ view }) => {
|
|
155
|
+
const action = view.action;
|
|
156
|
+
const required = action === undefined || !Object.hasOwn(SKILL_ACTION_REQUIRED_FIELDS, action)
|
|
157
|
+
? []
|
|
158
|
+
: SKILL_ACTION_REQUIRED_FIELDS[action] ?? [];
|
|
159
|
+
const missing = required.filter(field => view.scalars[field] === undefined || view.scalars[field] === null);
|
|
160
|
+
if (missing.length === 0)
|
|
161
|
+
return null;
|
|
162
|
+
return `skill_manage ${action} requires ${missing.join(', ')}; the tool description lists the arguments per action.`;
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* v30 REV-02: the immutable policy's protected list is enforced at plan validation, but the
|
|
167
|
+
* replay channel executes STORED plans — one accepted under an older policy must not land on a
|
|
168
|
+
* skill the CURRENT policy protects. Foreground (operator) writes are deliberately not gated by
|
|
169
|
+
* this list, and a deployment without the policy row keeps the previous behavior.
|
|
170
|
+
*/
|
|
171
|
+
const POLICY_PROTECTION = {
|
|
172
|
+
id: 'policy-protection',
|
|
173
|
+
appliesTo: ['admission', 'execution'],
|
|
174
|
+
run: ({ view, origin, protectedNames }) => {
|
|
175
|
+
if (origin === 'foreground')
|
|
176
|
+
return null;
|
|
177
|
+
if (view.name === '' || !protectedNames?.includes(view.name))
|
|
178
|
+
return null;
|
|
179
|
+
return `skill_manage: "${view.name}" is protected by the current policy (protectedSkillNames); replayed/autonomous writes are refused.`;
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Read-before-write (design §4.2): a non-foreground write must have read its target in the
|
|
184
|
+
* session that authored it, so a subagent or the review's own model cannot blind-overwrite a body
|
|
185
|
+
* it never saw. The read set comes from `collectReadSkillNames`, which counts only a read that did
|
|
186
|
+
* not fail — a failed or timed-out read does not pass this gate.
|
|
187
|
+
*
|
|
188
|
+
* Admission only. The reads belong to the WRITING session, and the replayed record has none: it
|
|
189
|
+
* passed this gate at its own staging point, or the review's plan filter on the plan path.
|
|
190
|
+
*
|
|
191
|
+
* An unreadable session log is NOT an empty read set: the write proceeds with one warning, the
|
|
192
|
+
* same soft-probe posture as the cross-source check, because a deployment whose sessions expose no
|
|
193
|
+
* log must not refuse every autonomous write.
|
|
194
|
+
*/
|
|
195
|
+
const READ_BEFORE_WRITE = {
|
|
196
|
+
id: 'read-before-write',
|
|
197
|
+
appliesTo: ['admission'],
|
|
198
|
+
run: ({ view, origin, readNames, warn }) => {
|
|
199
|
+
if (origin === 'foreground')
|
|
200
|
+
return null;
|
|
201
|
+
if (readNames === undefined) {
|
|
202
|
+
warn('skill_manage: the read-before-write check could not run for this call — the session log is not readable here, so the write is allowed.');
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
// A missing action is NOT this gate's business: the tool schema requires one, so an absent
|
|
206
|
+
// action is a malformed call whose refusal the LIBRARY owns ("Unknown action") — answering
|
|
207
|
+
// E-318 would send the model to read a skill for a call that cannot work (review 2026-09-26,
|
|
208
|
+
// P2). The rule's default-to-`patch` (V6-26) stays where it belongs, on the plan path.
|
|
209
|
+
if (view.action === undefined)
|
|
210
|
+
return null;
|
|
211
|
+
if (!isUnreadWrite({ action: view.action, name: view.name }, readNames))
|
|
212
|
+
return null;
|
|
213
|
+
return errorText('e-318-skill-write-without-a-read', { a1: view.name });
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
/**
|
|
217
|
+
* The confirm gate (design §4.1): a foreground create, or a bare foreground delete, is
|
|
218
|
+
* irreversible and gets ONE question. Admission only — a replayed record already carries the human
|
|
219
|
+
* release that staged it, so asking there would be the second interruption for one write.
|
|
220
|
+
*
|
|
221
|
+
* `delete` with `absorbed_into` is exempt: that is the merge protocol (an umbrella absorbs the source),
|
|
222
|
+
* and asking once per absorbed skill would turn one merge into N questions. A BARE delete is the
|
|
223
|
+
* destructive primitive the plan layer reserves for a human — a review may only delete into an
|
|
224
|
+
* umbrella (core's `SKILL_ACTION_REQUIRED_FIELDS` docblock).
|
|
225
|
+
*
|
|
226
|
+
* The gate is UX, not a security door (those are policy protection and read-before-write): the seam
|
|
227
|
+
* is required to be total, so an unmounted question service or a caller that is not the live root
|
|
228
|
+
* agent PROCEEDS — the operator's own session is the authority that asked for the write.
|
|
229
|
+
*
|
|
230
|
+
* Whether the question is put at all is CONFIGURED (`skillWriteConfirm`): `auto` writes without
|
|
231
|
+
* asking, `ask` waits for an answer indefinitely (what this gate did before the knob existed), and
|
|
232
|
+
* `timeout` asks but cancels the write on its own deadline. `auto` is the default because an
|
|
233
|
+
* unattended run — a background pass, a scheduled review, a headless session — must not park a tool
|
|
234
|
+
* call on a question nobody will answer.
|
|
235
|
+
*/
|
|
236
|
+
const HUMAN_CONFIRM = {
|
|
237
|
+
id: 'human-confirm',
|
|
238
|
+
appliesTo: ['admission'],
|
|
239
|
+
run: async ({ view, origin, confirm, warn, confirmMode, confirmTimeoutSeconds, describeTarget }) => {
|
|
240
|
+
const action = view.action;
|
|
241
|
+
if (action === undefined || origin !== 'foreground' || view.name === '')
|
|
242
|
+
return null;
|
|
243
|
+
const destructive = action === 'create' || (action === 'delete' && view.absorbedInto === undefined);
|
|
244
|
+
if (!destructive)
|
|
245
|
+
return null;
|
|
246
|
+
const mode = confirmMode ?? DEFAULT_WRITE_CONFIRM_MODE;
|
|
247
|
+
if (mode === 'auto')
|
|
248
|
+
return null;
|
|
249
|
+
if (confirm === undefined) {
|
|
250
|
+
warn('skill_manage: no confirmation channel is mounted, so the write proceeds unconfirmed.');
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
const confirmLabel = action === 'create' ? '创建' : '删除';
|
|
254
|
+
const seconds = timeoutSecondsOf(confirmTimeoutSeconds);
|
|
255
|
+
const cancel = new AbortController();
|
|
256
|
+
// The deadline is enforced HERE, not only carried by the signal: the signal lets the client
|
|
257
|
+
// take the card down, while the race guarantees the call returns even if nothing is listening
|
|
258
|
+
// to it — a prompt that outlives its deadline is the parked tool call this mode exists to
|
|
259
|
+
// prevent.
|
|
260
|
+
let timer;
|
|
261
|
+
const deadline = mode === 'timeout'
|
|
262
|
+
? new Promise((resolve) => {
|
|
263
|
+
timer = setTimeout(() => {
|
|
264
|
+
cancel.abort();
|
|
265
|
+
resolve(CONFIRM_TIMED_OUT);
|
|
266
|
+
}, seconds * 1000);
|
|
267
|
+
// Never hold the process open: a one-shot CLI run must not linger for the rest of the
|
|
268
|
+
// window after the write already returned. A server keeps the loop alive anyway, which is
|
|
269
|
+
// where the deadline does its work.
|
|
270
|
+
timer.unref();
|
|
271
|
+
})
|
|
272
|
+
: undefined;
|
|
273
|
+
// The question says what the OPERATOR needs: the consequence, then the target in its own words.
|
|
274
|
+
// Asking for a decision by name alone is asking someone to authorize an unknown artifact.
|
|
275
|
+
const context = await describeTarget?.();
|
|
276
|
+
const details = [
|
|
277
|
+
context?.description === undefined || context.description === '' ? null : '功能:' + context.description,
|
|
278
|
+
context?.versions === undefined ? null : '已记录的内容版本数:' + String(context.versions),
|
|
279
|
+
].filter((line) => line !== null);
|
|
280
|
+
const consequence = action === 'create'
|
|
281
|
+
? `创建技能 "${view.name}"?会在家族目录树里新建一个技能目录。`
|
|
282
|
+
: `删除技能 "${view.name}"?它会被归档到 .archive 并离开目录。`;
|
|
283
|
+
const asked = confirm({
|
|
284
|
+
action,
|
|
285
|
+
name: view.name,
|
|
286
|
+
confirmLabel,
|
|
287
|
+
question: {
|
|
288
|
+
id: CONFIRM_QUESTION_ID,
|
|
289
|
+
header: '确认',
|
|
290
|
+
// ONE line: the platform's question composer does not preserve newlines, so a multi-line
|
|
291
|
+
// question arrives as a run-on sentence. The facts are separated instead.
|
|
292
|
+
question: details.length === 0 ? consequence : consequence + ' ' + details.join(' · '),
|
|
293
|
+
options: [{ label: confirmLabel }, { label: CANCEL_LABEL }],
|
|
294
|
+
},
|
|
295
|
+
...mode === 'timeout' ? { signal: cancel.signal } : {},
|
|
296
|
+
});
|
|
297
|
+
let outcome;
|
|
298
|
+
try {
|
|
299
|
+
outcome = deadline === undefined ? await asked : await Promise.race([asked, deadline]);
|
|
300
|
+
}
|
|
301
|
+
finally {
|
|
302
|
+
if (timer !== undefined)
|
|
303
|
+
clearTimeout(timer);
|
|
304
|
+
}
|
|
305
|
+
if (outcome === true)
|
|
306
|
+
return null;
|
|
307
|
+
// Running out of time is not the same answer as being declined: the write was cancelled
|
|
308
|
+
// because nobody was there to decide, and the refusal names the knob that changes that. Which
|
|
309
|
+
// promise the race returned first is a microtask-order detail — a platform that answers the
|
|
310
|
+
// abort with its own dismissal can settle `asked` first — so the case is decided by the
|
|
311
|
+
// deadline's signal, which only this gate aborts.
|
|
312
|
+
if (outcome === CONFIRM_TIMED_OUT || cancel.signal.aborted) {
|
|
313
|
+
return errorText('e-319-skill-write-confirm-timed-out', {
|
|
314
|
+
a1: view.name,
|
|
315
|
+
a2: action === 'create' ? '创建' : '删除',
|
|
316
|
+
a3: String(seconds),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
return errorText('e-317-skill-write-not-confirmed', {
|
|
320
|
+
a1: view.name,
|
|
321
|
+
a2: action === 'create' ? '创建' : '删除',
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
};
|
|
325
|
+
/** The sequence, in order. A gate whose `appliesTo` omits the asking point is skipped. */
|
|
326
|
+
const GATES = [ARGUMENT_SHAPE, MISSING_ARGS, POLICY_PROTECTION, READ_BEFORE_WRITE, HUMAN_CONFIRM];
|
|
327
|
+
/**
|
|
328
|
+
* Run the sequence for one write attempt.
|
|
329
|
+
* @param context - the asking point, the write, and the seams the human-facing gate needs.
|
|
330
|
+
* @returns the FIRST refusal, or null when every applicable gate passed. Both points return the
|
|
331
|
+
* message as the tool result unchanged, so the two entries refuse with identical wording.
|
|
332
|
+
*/
|
|
333
|
+
export async function runWriteGates(context) {
|
|
334
|
+
const input = { ...context, view: gateArgsOf(context.args) };
|
|
335
|
+
for (const gate of GATES) {
|
|
336
|
+
if (!gate.appliesTo.includes(context.point))
|
|
337
|
+
continue;
|
|
338
|
+
const refusal = await gate.run(input);
|
|
339
|
+
if (refusal !== null)
|
|
340
|
+
return refusal;
|
|
341
|
+
}
|
|
342
|
+
return null;
|
|
343
|
+
}
|
|
344
|
+
//# sourceMappingURL=write-gates.js.map
|