@t4r71/dsh-dual-axis 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,34 @@
1
+ import type { Context } from '@deepseek-ai/cordis';
2
+ import z from '@deepseek-ai/schemastery';
3
+ /** 插件名,loader 诊断用。 */
4
+ export declare const name = "dual-axis-read-guard";
5
+ /** 构造期就要用的服务:缺一个就装载失败,而不是运行期静默放行。 */
6
+ export declare const inject: readonly ["fs", "tools"];
7
+ /** 插件配置。 */
8
+ export interface Config {
9
+ /** 关闭后本插件完全不注册。 */
10
+ enabled?: boolean;
11
+ /** 被检查的读入口工具名。 */
12
+ readTools?: string[];
13
+ /** 被检查的写入口工具名。 */
14
+ writeTools?: string[];
15
+ }
16
+ /** 装载期校验的配置。 */
17
+ export declare const Config: z<Config>;
18
+ /**
19
+ * 注册两层的轴围栏。
20
+ * @param ctx - 插件上下文;两个注册都挂在它上面,随它一起卸载。
21
+ * @param config - 装载期已校验的配置。
22
+ */
23
+ declare function apply(ctx: Context, config?: Config): Promise<void>;
24
+ /**
25
+ * 默认导出必须自带 inject / Config / name:Loader.unwrapExports 返回 exports.default 并丢掉
26
+ * 兄弟具名导出(vendor/loader/src/index.ts:201-208),而 registry 只读 plugin.inject
27
+ * (vendor/cordis/src/registry.ts:330)—— 挂不上就会不等服务先激活,首次读 ctx.tools 抛错。
28
+ */
29
+ declare const _default: typeof apply & {
30
+ inject: readonly ["fs", "tools"];
31
+ Config: z<Config>;
32
+ };
33
+ export default _default;
34
+ //# sourceMappingURL=read-guard.d.ts.map
@@ -0,0 +1,130 @@
1
+ /**
2
+ * The model-facing text of the two access axes: ONE source for both the
3
+ * runtime-context paragraph the model reads before it acts and the refusal a
4
+ * fence returns when it acts anyway.
5
+ *
6
+ * Why one module: the paragraph and the refusal must state the same range. Two
7
+ * hand-written copies drift, and the drift is invisible — the model would plan
8
+ * against one boundary in its context and be denied by a different one. Both
9
+ * callers therefore call {@link renderAxisRange} for the range sentence and
10
+ * share {@link NO_BYPASS} and {@link WIDENING_EXIT} verbatim; neither string is
11
+ * spelled anywhere else in this package.
12
+ *
13
+ * Both callers feed it only session-log facts plus the group definitions the
14
+ * log's ids resolve against: the axis preset is the `dual-axis/scopes` fold (the
15
+ * projection unit), the workspace root is the session header's `cwd`, and the
16
+ * group library is the settings row's `groups` field. Nothing here reads ambient
17
+ * state of its own, so the rendered text is reconstructable from the log and
18
+ * that library.
19
+ *
20
+ * @module @t4r71/dsh-dual-axis/scope-prompt
21
+ */
22
+ import type { AxisScope } from './axis.ts';
23
+ import type { EffectiveAxes, WriteNarrowing } from './groups.ts';
24
+ /**
25
+ * The sentence naming what is NOT a way around a refusal. Three facts, all of
26
+ * which the model otherwise has to guess: the denial is the session's rule
27
+ * rather than the calling tool's limitation; where each axis is enforced; and
28
+ * that the two axes are layered differently.
29
+ *
30
+ * The layering is stated per axis because asserting it for the whole fence
31
+ * would be false. `read-guard.ts` enforces the read axis and the allow/deny
32
+ * path entries of the effective write range at the tool-dispatch layer, and the
33
+ * write BASE tier a second time below it: `session-axes.ts` mirrors the
34
+ * narrower of the two bases onto the session `sandbox/mode`, which the command
35
+ * tools themselves run under. An effective write range based on `all` mirrors
36
+ * to `danger-full-access`, so for that base there is no layer below the tool
37
+ * layer at all.
38
+ */
39
+ export declare const NO_BYPASS: string;
40
+ /**
41
+ * The sentence naming the only two compliant moves. Both are user-facing, so it
42
+ * names where the user acts: the settings page row and the two composer
43
+ * dropdowns are the two surfaces that write the same session axes.
44
+ */
45
+ export declare const WIDENING_EXIT: string;
46
+ /**
47
+ * Render one axis as the concrete places it permits and forbids, with the
48
+ * configured entries already resolved to canonical absolute roots.
49
+ *
50
+ * A model plans against directories, not at mode names: `workspace` alone
51
+ * leaves it guessing whether the platform temp areas count, and `custom`
52
+ * leaves it guessing which paths were added or removed. {@link resolveScope}
53
+ * answers both, so this prints its result instead of the axis value.
54
+ *
55
+ * A `custom` axis whose entries cannot be resolved (a foreign or hand-edited
56
+ * log carrying a relative path, which `ScopeConfigError` refuses) is stated as
57
+ * permitting nothing rather than throwing: the paragraph must not fail the
58
+ * whole assembly, and a range that cannot be evaluated must never read as a
59
+ * wider one.
60
+ * @param axis - the axis value in force.
61
+ * @param workspaceRoot - the session workspace root a `workspace` and `custom` axis resolve against.
62
+ * @returns the range sentence, e.g. `kind custom; allowed roots: ["C:\\ws"]; denied roots: ["C:\\ws\\secret"] (a denied path always wins)`.
63
+ */
64
+ export declare function renderAxisRange(axis: AxisScope, workspaceRoot: string): string;
65
+ /**
66
+ * The sentence reporting what the write-never-exceeds-read invariant removed.
67
+ *
68
+ * Stated because the removal is otherwise invisible: a rule group that grants
69
+ * write access to a directory the read axis does not cover grants nothing, and a
70
+ * user who cannot see that concludes the rule was ignored. Empty when the
71
+ * intersection removed nothing, so a session that narrowed nothing is not
72
+ * warned about a narrowing that did not happen.
73
+ * @param narrowing - what the resolution removed from the write axis.
74
+ * @returns the sentence, or the empty string when nothing was removed.
75
+ */
76
+ export declare function renderNarrowing(narrowing: WriteNarrowing): string;
77
+ /**
78
+ * The runtime-context paragraph naming both axes as they stand for one session.
79
+ *
80
+ * The write member is the EFFECTIVE write range: the write axis with every
81
+ * referenced group expanded, intersected with the read axis, and the intersection
82
+ * is stated rather than left for the model to infer. A model that assumed the
83
+ * write axis stood alone would plan writes the session refuses; one that assumed
84
+ * the intersection without being told would refuse work the axes permit.
85
+ * @param axes - the effective axis pair for the session.
86
+ * @param workspaceRoot - the session's workspace root.
87
+ * @returns the paragraph, ready to be contributed as one runtime-context entry.
88
+ */
89
+ export declare function renderScopePrompt(axes: EffectiveAxes, workspaceRoot: string): string;
90
+ /**
91
+ * The notice stating that a session's axis preset could not be resolved, so no
92
+ * path is known to be allowed.
93
+ *
94
+ * Rendered both into the prompt and into a refusal, from this one source: an
95
+ * unresolvable reference — a rule group deleted while a session still references
96
+ * it — must fail closed everywhere it is met, and must say which id is missing
97
+ * rather than behaving like a group that allows nothing in particular.
98
+ * @param problem - the resolution failure, naming the offending id.
99
+ * @returns the model-facing notice.
100
+ */
101
+ export declare function unresolvedAxesNotice(problem: string): string;
102
+ /** What one refusal has to name. */
103
+ export interface ScopeRefusal {
104
+ /** The refused path, as the model spelled it or as the caller resolved it. */
105
+ readonly displayPath: string;
106
+ /** Which axis refused. */
107
+ readonly axis: 'read' | 'write';
108
+ /**
109
+ * The refusing range's value. A write refusal carries the EFFECTIVE write
110
+ * range, so the refusal states the boundary the session actually enforces
111
+ * rather than the write axis it was derived from.
112
+ */
113
+ readonly scope: AxisScope;
114
+ /** The workspace root that axis resolves against. */
115
+ readonly workspaceRoot: string;
116
+ /**
117
+ * What the read axis removed from the write range, when the refusal is a
118
+ * write. Stated on the refusal as well as in the prompt: the model is told why
119
+ * the write axis it was given does not reach this path.
120
+ */
121
+ readonly narrowing?: WriteNarrowing;
122
+ }
123
+ /**
124
+ * The refusal a fence returns for one path, built from the same range sentence
125
+ * the prompt uses.
126
+ * @param input - the refused path, the axis that refused it, and the root it resolves against.
127
+ * @returns the model-facing refusal text.
128
+ */
129
+ export declare function scopeRefusal(input: ScopeRefusal): string;
130
+ //# sourceMappingURL=scope-prompt.d.ts.map
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The shared path-range ALGEBRA behind both access axes: one pure evaluation
3
+ * from an {@link AxisScope} to the canonical allow and deny root sets the
4
+ * fence consumes.
5
+ *
6
+ * This is the 0.1.7 port of 0.1.6's `@deepseek-ai/dsh-sandbox/scope`
7
+ * (152 lines), reduced to what a fence needs and made source-agnostic:
8
+ *
9
+ * - `workspace` derives its roots from 0.1.7's own
10
+ * `writableRoots(policy)` (`@deepseek-ai/dsh-sandbox`, re-exported from
11
+ * `packages/sandbox/sandbox/src/roots.ts:52`), so the fence agrees with
12
+ * the Seatbelt profile and the write fence by construction.
13
+ * - Containment itself is NOT evaluated here. 0.1.6 took the enforcement
14
+ * layer's `contains` predicate as a parameter; this port keeps that shape
15
+ * but narrows it to the SYNCHRONOUS lexical predicate the pure tests use,
16
+ * and the filesystem-identity fallback lives in the fence
17
+ * (`fs-fence.ts`), which is where the canonical target key exists.
18
+ *
19
+ * `resolveScope` returns the sets; the fence applies them with deny-wins
20
+ * precedence through {@link scopeContains}.
21
+ *
22
+ * @module @t4r71/dsh-dual-axis/scope
23
+ */
24
+ import type { AxisScope } from './axis.ts';
25
+ /** The scope inputs that do not depend on the session: just its workspace root. */
26
+ export interface ScopePolicy {
27
+ /** Absolute root `workspace` scopes derive from. */
28
+ workspaceRoot: string;
29
+ }
30
+ /**
31
+ * One axis's evaluated range. Both sets hold canonical absolute paths; an
32
+ * empty `allow` means "nothing is permitted" unless {@link unbounded} is set,
33
+ * because the fence treats the allow-list as exhaustive.
34
+ */
35
+ export interface ResolvedScope {
36
+ /**
37
+ * Whether this axis permits everything except {@link deny} — the `all` base.
38
+ * A flag, NOT a root path: "no containing boundary" cannot be spelled as a
39
+ * path. `canonicalPath('/')` resolves to the CURRENT DRIVE's root on Windows
40
+ * (measured: `M:\` for a session on `M:`), which would silently restrict an
41
+ * unbounded axis to one volume and deny every other drive — the exact
42
+ * opposite of what the axis means. Representing it as a path also cannot
43
+ * survive {@link scopeContains}'s identity comparisons, which are per-volume.
44
+ */
45
+ unbounded: boolean;
46
+ /** Roots this axis permits (the target must be one of them or lie beneath one); ignored when {@link unbounded}. */
47
+ allow: readonly string[];
48
+ /** Roots this axis forbids even when an allow root contains them — removal wins. */
49
+ deny: readonly string[];
50
+ }
51
+ /** Thrown when a configured `custom` scope entry cannot name an absolute path. */
52
+ export declare class ScopeConfigError extends Error {
53
+ /** Which custom entry was malformed (`allow` or `deny`). */
54
+ readonly entry: 'allow' | 'deny';
55
+ /** The offending value as configured. */
56
+ readonly value: string;
57
+ constructor(
58
+ /** Which custom entry was malformed (`allow` or `deny`). */
59
+ entry: 'allow' | 'deny',
60
+ /** The offending value as configured. */
61
+ value: string);
62
+ }
63
+ /**
64
+ * Whether a configured path is spelled absolutely on this host. Both POSIX
65
+ * (`/x`) and Windows (`C:\\x`, `\\\\server\\share`) spellings are accepted
66
+ * because a policy may be authored for one world and resolved in another; the
67
+ * canonical resolution that follows is what actually binds it to this host.
68
+ * @param path - the configured path spelling.
69
+ * @returns whether the spelling is absolute.
70
+ */
71
+ export declare function isAbsoluteSpelling(path: string): boolean;
72
+ /**
73
+ * Evaluate one axis against a workspace root. `deny` permits nothing;
74
+ * `workspace` yields the shared writable roots; `all` is unbounded;
75
+ * `custom` yields its base plus its own additions, with its removals listed
76
+ * separately so the fence can apply them with precedence.
77
+ *
78
+ * A `custom` scope whose base is `all` stays unbounded: "everything except
79
+ * these directories" is still everything-except, so its removals must survive
80
+ * into {@link ResolvedScope.deny} rather than being flattened into an allow
81
+ * list that could not express them.
82
+ * @param scope - the axis value.
83
+ * @param policy - the workspace root `workspace` and `custom` scopes resolve against.
84
+ * @returns the evaluated range.
85
+ * @throws {ScopeConfigError} when a `custom` entry is not a non-empty absolute path.
86
+ */
87
+ export declare function resolveScope(scope: AxisScope, policy: ScopePolicy): ResolvedScope;
88
+ /**
89
+ * Whether `target` is the root itself or lies beneath it, by canonical
90
+ * spelling. Case-insensitive on Windows, matching that filesystem's
91
+ * convention.
92
+ * @param target - canonical target path.
93
+ * @param root - canonical root path.
94
+ * @param caseSensitive - whether lexical comparison preserves case; defaults to the host convention.
95
+ * @returns whether the target is the root or a descendant of it.
96
+ */
97
+ export declare function isLexicallyUnder(target: string, root: string, caseSensitive?: boolean): boolean;
98
+ /**
99
+ * Whether `targetKey` is permitted by an evaluated scope. Removal wins over
100
+ * addition: a deny root containing the target refuses it even when an allow
101
+ * root — or an unbounded axis — would otherwise permit it.
102
+ * @param scope - the evaluated scope.
103
+ * @param targetKey - the target's canonical identity key (the resolved path).
104
+ * @param contains - the containment predicate; defaults to {@link isLexicallyUnder}.
105
+ * @returns whether the scope permits the target.
106
+ */
107
+ export declare function scopeContains(scope: ResolvedScope, targetKey: string, contains?: (path: string, root: string) => boolean): boolean;
108
+ //# sourceMappingURL=scope.d.ts.map
@@ -0,0 +1,51 @@
1
+ /**
2
+ * The write axis's landing place in a session's LOG, and the reason it is the
3
+ * only one.
4
+ *
5
+ * 0.1.6 carried both axes on the \`sandbox/mode\` event (\`readScope\` /
6
+ * \`writeScope\` members) and wrote them through \`setSandboxScopes\`. 0.1.7's
7
+ * \`sandbox/mode\` payload is locked to \`{ mode, source }\` in three independent
8
+ * places — the \`SessionEventMap\` declaration
9
+ * (\`packages/sandbox/sandbox-policy/src/session-mode.ts:33-38\`), the session-format
10
+ * validator
11
+ * (\`packages/session/session-format-v0-to-v1/src/payload-validation.ts:165-168\`),
12
+ * and the generated schema (\`docs/persistence-schema.json:11132-11147\`) — so the
13
+ * path-level axis pair cannot ride on it.
14
+ *
15
+ * A package-declared event type is not an alternative: \`Session.append\`
16
+ * (\`packages/core/session/src/index.ts:722-750\`) builds the envelope as exactly
17
+ * \`{ type, seq, time, data, surfaceOp?, sourceEventSeqs? }\` and takes no envelope
18
+ * option, so a type outside \`KNOWN_SESSION_EVENT_TYPES\` cannot be marked
19
+ * \`ignorable\` (the marker exists only on the READ side,
20
+ * \`packages/session/session-persistence/src/storage-contract.ts:75\`) and a log
21
+ * carrying it is refused whole. That refusal is why the pair now lives in
22
+ * \`./session-store.ts\` instead.
23
+ *
24
+ * What REMAINS here is the write base's mirror onto \`sandbox/mode\`: a known type,
25
+ * carrying the closed three-value mode the operating-system layer enforces. The
26
+ * mirror is the NARROWER of the two axes' bases, never the write axis's own —
27
+ * the effective write range is the write axis intersected with the read axis, so
28
+ * mirroring the wider base would leave the layer below the tool fence granting
29
+ * writes the intersection forbids.
30
+ *
31
+ * @module @t4r71/dsh-dual-axis/session-axes
32
+ */
33
+ import type { Session } from '@deepseek-ai/dsh-session';
34
+ import type { EffectiveScopes } from './axis.ts';
35
+ /**
36
+ * Mirror one session's write base onto 0.1.7's own \`sandbox/mode\` event, so the
37
+ * inherited write fence and the system-prompt policy section agree with the axis
38
+ * pair this package stores.
39
+ *
40
+ * The mirrored TIER is derived from the two axes' bases only, never from a rule
41
+ * group's contents, so editing a group cannot leave this event stale. Path-level
42
+ * narrowing is enforced live, where the group definitions are read.
43
+ *
44
+ * The event is appended even when it repeats the standing mode: \`sandbox/mode\`
45
+ * carries no idempotence contract, and a session that gained axes without a
46
+ * matching mode record would have its write fence read a stale mode.
47
+ * @param session - the session the axes belong to.
48
+ * @param axes - the axis pair in force from this event onward.
49
+ */
50
+ export declare function mirrorWriteMode(session: Session, axes: EffectiveScopes): void;
51
+ //# sourceMappingURL=session-axes.d.ts.map