@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.
- package/LICENSE +21 -0
- package/README.md +147 -0
- package/cordis.patch.yml +68 -0
- package/lib/fs.js +419 -0
- package/lib/index.js +2591 -0
- package/lib/read-guard.js +1812 -0
- package/lib/session-store.js +978 -0
- package/lib/types/axis-command.d.ts +59 -0
- package/lib/types/axis-entry.d.ts +45 -0
- package/lib/types/axis.d.ts +131 -0
- package/lib/types/config.d.ts +173 -0
- package/lib/types/containment.d.ts +27 -0
- package/lib/types/content.d.ts +68 -0
- package/lib/types/fs-fence.d.ts +196 -0
- package/lib/types/fs.d.ts +15 -0
- package/lib/types/groups.d.ts +226 -0
- package/lib/types/index.d.ts +205 -0
- package/lib/types/read-guard.d.ts +34 -0
- package/lib/types/scope-prompt.d.ts +130 -0
- package/lib/types/scope.d.ts +108 -0
- package/lib/types/session-axes.d.ts +51 -0
- package/lib/types/session-store.d.ts +496 -0
- package/package.json +103 -0
|
@@ -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
|