@elinpf/dsh-ops-access 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/README.i18n.yaml +4 -0
- package/README.md +42 -0
- package/README.zh.md +42 -0
- package/cordis.patch.yml +1 -0
- package/lib/index.d.ts +75 -0
- package/lib/index.js +1008 -0
- package/lib/invariant.d.ts +16 -0
- package/lib/invariant.js +28 -0
- package/lib/mention.d.ts +59 -0
- package/lib/mention.js +93 -0
- package/lib/types.d.ts +268 -0
- package/lib/types.js +10 -0
- package/package.json +73 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant companion for @elinpf/dsh-ops-access.
|
|
3
|
+
*
|
|
4
|
+
* @module @elinpf/dsh-ops-access/invariant
|
|
5
|
+
*/
|
|
6
|
+
/** Cordis companion plugin name. */
|
|
7
|
+
declare const name = "ops-access-invariant";
|
|
8
|
+
/** Service required before the companion can reserve package ownership. */
|
|
9
|
+
declare const inject: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Register this package's invariant companion.
|
|
12
|
+
* @param ctx - Context carrying the invariant service.
|
|
13
|
+
* @returns a promise resolving after registration.
|
|
14
|
+
*/
|
|
15
|
+
declare const apply: (ctx: any) => Promise<void>;
|
|
16
|
+
export { apply, inject, name };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Invariant companion for @elinpf/dsh-ops-access.
|
|
3
|
+
*
|
|
4
|
+
* @module @elinpf/dsh-ops-access/invariant
|
|
5
|
+
*/
|
|
6
|
+
const PACKAGE_NAME = '@elinpf/dsh-ops-access';
|
|
7
|
+
/** Cordis companion plugin name. */
|
|
8
|
+
const name = 'ops-access-invariant';
|
|
9
|
+
/** Service required before the companion can reserve package ownership. */
|
|
10
|
+
const inject = ['invariants'];
|
|
11
|
+
/**
|
|
12
|
+
* No runtime invariant: this package owns no session event types and no
|
|
13
|
+
* projection — its durable state is the YAML registry file, which is
|
|
14
|
+
* re-read, re-parsed, and re-validated on EVERY call (no cache to drift),
|
|
15
|
+
* and writes are validated against the provider schema before they land.
|
|
16
|
+
* The in-memory state (provider/broker registrations) is fiber-scoped and
|
|
17
|
+
* torn down by effect disposal, so there is nothing to guard at runtime.
|
|
18
|
+
*/
|
|
19
|
+
const install = () => { };
|
|
20
|
+
/**
|
|
21
|
+
* Register this package's invariant companion.
|
|
22
|
+
* @param ctx - Context carrying the invariant service.
|
|
23
|
+
* @returns a promise resolving after registration.
|
|
24
|
+
*/
|
|
25
|
+
const apply = async (ctx) => {
|
|
26
|
+
ctx.invariants.register(PACKAGE_NAME, install);
|
|
27
|
+
};
|
|
28
|
+
export { apply, inject, name };
|
package/lib/mention.d.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-access mention encoding — the structured form of an `@` pick on an
|
|
3
|
+
* access profile, mirroring session-reference's `dsh-session:` pattern.
|
|
4
|
+
*
|
|
5
|
+
* A mention is a host-neutral Markdown span `@[kind/name](dsh-access:<payload>)`
|
|
6
|
+
* where the payload is base64url(JSON [kind, name]). The composer draft shows
|
|
7
|
+
* the label; on submit the full mention travels in the message text; the
|
|
8
|
+
* preset-plane `agent/pre-step` listener (src/index.ts) parses it back out,
|
|
9
|
+
* rewrites the text to a readable `@kind/name`, and injects the profiles'
|
|
10
|
+
* envelope context.
|
|
11
|
+
*
|
|
12
|
+
* Host-side only — the browser never encodes or parses these; the remote
|
|
13
|
+
* hands it ready-made mention strings.
|
|
14
|
+
*
|
|
15
|
+
* @module @elinpf/dsh-ops-access/mention
|
|
16
|
+
*/
|
|
17
|
+
/** URI scheme reserved for ops access-profile references. */
|
|
18
|
+
export declare const ACCESS_REFERENCE_SCHEME = "dsh-access:";
|
|
19
|
+
/** One referenced access profile. */
|
|
20
|
+
export interface AccessReference {
|
|
21
|
+
kind: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
/** A parsed reference with its display label, in first-appearance order. */
|
|
25
|
+
export interface ParsedAccessReference extends AccessReference {
|
|
26
|
+
label: string;
|
|
27
|
+
}
|
|
28
|
+
/** Result of extracting mentions from plain text. */
|
|
29
|
+
export interface ParsedAccessReferenceText {
|
|
30
|
+
/** Text with opaque mention spans replaced by readable `@label`. */
|
|
31
|
+
text: string;
|
|
32
|
+
/** Structured references in first-appearance order (not deduplicated). */
|
|
33
|
+
references: ParsedAccessReference[];
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Encode a profile reference as a canonical lossless URI.
|
|
37
|
+
* @param reference - kind + name of the profile.
|
|
38
|
+
* @returns canonical `dsh-access:` URI.
|
|
39
|
+
*/
|
|
40
|
+
export declare function encodeAccessReferenceUri(reference: AccessReference): string;
|
|
41
|
+
/**
|
|
42
|
+
* Decode and canonicalize one access-reference URI.
|
|
43
|
+
* @param uri - complete canonical URI.
|
|
44
|
+
* @returns decoded kind + name.
|
|
45
|
+
*/
|
|
46
|
+
export declare function decodeAccessReferenceUri(uri: string): AccessReference;
|
|
47
|
+
/**
|
|
48
|
+
* Render the Markdown mention the composer inserts into the draft.
|
|
49
|
+
* @param reference - kind + name of the profile.
|
|
50
|
+
* @returns escaped `@[kind/name](uri)` mention.
|
|
51
|
+
*/
|
|
52
|
+
export declare function formatAccessMention(reference: AccessReference): string;
|
|
53
|
+
/**
|
|
54
|
+
* Extract Markdown mentions and bare canonical URIs from one text value,
|
|
55
|
+
* replacing them with readable `@label` spans.
|
|
56
|
+
* @param text - host text to normalize.
|
|
57
|
+
* @returns readable text and structured references in appearance order.
|
|
58
|
+
*/
|
|
59
|
+
export declare function parseAccessReferenceText(text: string): ParsedAccessReferenceText;
|
package/lib/mention.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-access mention encoding — the structured form of an `@` pick on an
|
|
3
|
+
* access profile, mirroring session-reference's `dsh-session:` pattern.
|
|
4
|
+
*
|
|
5
|
+
* A mention is a host-neutral Markdown span `@[kind/name](dsh-access:<payload>)`
|
|
6
|
+
* where the payload is base64url(JSON [kind, name]). The composer draft shows
|
|
7
|
+
* the label; on submit the full mention travels in the message text; the
|
|
8
|
+
* preset-plane `agent/pre-step` listener (src/index.ts) parses it back out,
|
|
9
|
+
* rewrites the text to a readable `@kind/name`, and injects the profiles'
|
|
10
|
+
* envelope context.
|
|
11
|
+
*
|
|
12
|
+
* Host-side only — the browser never encodes or parses these; the remote
|
|
13
|
+
* hands it ready-made mention strings.
|
|
14
|
+
*
|
|
15
|
+
* @module @elinpf/dsh-ops-access/mention
|
|
16
|
+
*/
|
|
17
|
+
/** URI scheme reserved for ops access-profile references. */
|
|
18
|
+
export const ACCESS_REFERENCE_SCHEME = 'dsh-access:';
|
|
19
|
+
/**
|
|
20
|
+
* Encode a profile reference as a canonical lossless URI.
|
|
21
|
+
* @param reference - kind + name of the profile.
|
|
22
|
+
* @returns canonical `dsh-access:` URI.
|
|
23
|
+
*/
|
|
24
|
+
export function encodeAccessReferenceUri(reference) {
|
|
25
|
+
const payload = Buffer.from(JSON.stringify([reference.kind, reference.name]), 'utf8').toString('base64url');
|
|
26
|
+
return `${ACCESS_REFERENCE_SCHEME}${payload}`;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Decode and canonicalize one access-reference URI.
|
|
30
|
+
* @param uri - complete canonical URI.
|
|
31
|
+
* @returns decoded kind + name.
|
|
32
|
+
*/
|
|
33
|
+
export function decodeAccessReferenceUri(uri) {
|
|
34
|
+
if (!uri.startsWith(ACCESS_REFERENCE_SCHEME)) {
|
|
35
|
+
throw invalidUri(uri);
|
|
36
|
+
}
|
|
37
|
+
const payload = uri.slice(ACCESS_REFERENCE_SCHEME.length);
|
|
38
|
+
if (!/^[A-Za-z0-9_-]+$/.test(payload))
|
|
39
|
+
throw invalidUri(uri);
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(Buffer.from(payload, 'base64url').toString('utf8'));
|
|
42
|
+
if (!Array.isArray(parsed) || parsed.length !== 2
|
|
43
|
+
|| typeof parsed[0] !== 'string' || typeof parsed[1] !== 'string') {
|
|
44
|
+
throw new TypeError('decoded payload is not a [kind, name] pair');
|
|
45
|
+
}
|
|
46
|
+
const reference = { kind: parsed[0], name: parsed[1] };
|
|
47
|
+
if (encodeAccessReferenceUri(reference) !== uri)
|
|
48
|
+
throw new TypeError('URI is not canonical');
|
|
49
|
+
return reference;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw invalidUri(uri, error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Render the Markdown mention the composer inserts into the draft.
|
|
57
|
+
* @param reference - kind + name of the profile.
|
|
58
|
+
* @returns escaped `@[kind/name](uri)` mention.
|
|
59
|
+
*/
|
|
60
|
+
export function formatAccessMention(reference) {
|
|
61
|
+
const label = escapeLabel(`${reference.kind}/${reference.name}`);
|
|
62
|
+
return `@[${label}](${encodeAccessReferenceUri(reference)})`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Extract Markdown mentions and bare canonical URIs from one text value,
|
|
66
|
+
* replacing them with readable `@label` spans.
|
|
67
|
+
* @param text - host text to normalize.
|
|
68
|
+
* @returns readable text and structured references in appearance order.
|
|
69
|
+
*/
|
|
70
|
+
export function parseAccessReferenceText(text) {
|
|
71
|
+
const references = [];
|
|
72
|
+
const pattern = /@\[((?:\\.|[^\\\]])*)\]\((dsh-access:[^\s)]*)\)|(dsh-access:[A-Za-z0-9_-]+)/gu;
|
|
73
|
+
const rendered = text.replace(pattern, (_match, rawLabel, markdownUri, bareUri) => {
|
|
74
|
+
const uri = markdownUri ?? bareUri;
|
|
75
|
+
/* v8 ignore next -- the two-alternative regex always captures exactly one URI group */
|
|
76
|
+
if (uri === undefined)
|
|
77
|
+
throw new Error('access reference URI is missing');
|
|
78
|
+
const reference = decodeAccessReferenceUri(uri);
|
|
79
|
+
const label = rawLabel === undefined ? `${reference.kind}/${reference.name}` : unescapeLabel(rawLabel);
|
|
80
|
+
references.push({ ...reference, label });
|
|
81
|
+
return `@${label}`;
|
|
82
|
+
});
|
|
83
|
+
return { text: rendered, references };
|
|
84
|
+
}
|
|
85
|
+
function escapeLabel(label) {
|
|
86
|
+
return label.replace(/[\\\]]/gu, match => `\\${match}`);
|
|
87
|
+
}
|
|
88
|
+
function unescapeLabel(label) {
|
|
89
|
+
return label.replace(/\\(.)/gu, '$1');
|
|
90
|
+
}
|
|
91
|
+
function invalidUri(uri, cause) {
|
|
92
|
+
return new Error(`invalid access reference URI ${JSON.stringify(uri)}`, cause === undefined ? undefined : { cause });
|
|
93
|
+
}
|
package/lib/types.d.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the ops-access capability seam.
|
|
3
|
+
*
|
|
4
|
+
* Types only — every runtime value (the `Config` schema, `expandHome`,
|
|
5
|
+
* `registerAccessProvider`/`registerAccessBroker`, the plugin itself) lives
|
|
6
|
+
* in index.ts.
|
|
7
|
+
*
|
|
8
|
+
* @module @elinpf/dsh-ops-access/types
|
|
9
|
+
*/
|
|
10
|
+
import type { ZodType } from 'zod';
|
|
11
|
+
export type { Config } from './index.js';
|
|
12
|
+
/** A credential-kind provider: the zod schema for its entries plus an optional processing step. */
|
|
13
|
+
export interface AccessProvider {
|
|
14
|
+
/** Credential kind, matching the registry section name ('k8s' | 'ceph' | 'ssh' | ...). */
|
|
15
|
+
kind: string;
|
|
16
|
+
/** Zod schema for one registry entry of this kind (excluding name and the envelope fields). */
|
|
17
|
+
schema: ZodType;
|
|
18
|
+
/** Optional post-validation processing (e.g. `~` expansion). Input is the schema-validated entry. */
|
|
19
|
+
process?(entry: unknown, name: string): Record<string, unknown>;
|
|
20
|
+
/**
|
|
21
|
+
* One-line human doc of the entry's fields, surfaced by `help()` so the
|
|
22
|
+
* agent can learn how to write a registry entry for this kind. Keep it to
|
|
23
|
+
* the fields themselves, e.g. "kubeconfig: path to the kubeconfig file (~
|
|
24
|
+
* is expanded)". The schema is the machine contract; this is its prose.
|
|
25
|
+
*/
|
|
26
|
+
fieldsDoc?: string;
|
|
27
|
+
/**
|
|
28
|
+
* How to DERIVE a read-only (ro) credential from the rw one for this kind
|
|
29
|
+
* (the kubectl/ceph command sequence), including the naming convention for
|
|
30
|
+
* the derived account. Surfaced by help(); the register_access tool points
|
|
31
|
+
* the agent at it. Prose, not code — the agent executes the recipe with
|
|
32
|
+
* judgment; exact commands drift with infrastructure versions.
|
|
33
|
+
*/
|
|
34
|
+
derivationDoc?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Field names whose values are file PATHS pointing to credential material
|
|
37
|
+
* (e.g. kubeconfig, ceph.conf, keyring, SSH private key). When the admin
|
|
38
|
+
* UI receives CONTENT for these fields (instead of a path), it writes the
|
|
39
|
+
* content to a managed file and stores the resulting path in the registry.
|
|
40
|
+
* Fields not listed here are inline values stored as-is (e.g. ssh host, user, port).
|
|
41
|
+
*/
|
|
42
|
+
fileFields?: string[];
|
|
43
|
+
/**
|
|
44
|
+
* Save-time validator for a file field's pasted CONTENT, run before the
|
|
45
|
+
* content is written to disk (the admin UI route and the register_access
|
|
46
|
+
* tool share this check). Return an error message to reject the write,
|
|
47
|
+
* nothing to accept. May be ASYNC — a provider may run a real local
|
|
48
|
+
* parser (ssh passes the paste through ssh-keygen -y, the same parser ssh
|
|
49
|
+
* runs at connection time). Receives the content AFTER provider-declared
|
|
50
|
+
* normalization (normalizeTrailingNewline), i.e. exactly the bytes that
|
|
51
|
+
* will land on disk. Keep it structural — format and shape only (a ceph
|
|
52
|
+
* keyring has an indented, base64-decodable key line; a kubeconfig parses
|
|
53
|
+
* as YAML with clusters/contexts/users), never connectivity or value
|
|
54
|
+
* judgments. Catching paste corruption here beats a cryptic CLI parse
|
|
55
|
+
* error at use time.
|
|
56
|
+
*/
|
|
57
|
+
validateContent?: (field: string, content: string) => string | null | undefined | Promise<string | null | undefined>;
|
|
58
|
+
/**
|
|
59
|
+
* When true, file-field content is normalized to end with exactly one
|
|
60
|
+
* trailing newline BEFORE validation and write. PEM/armored formats
|
|
61
|
+
* require the END line newline-terminated, and pastes / model transcripts
|
|
62
|
+
* routinely drop that last byte (2026-08-27: a registered ssh key failed
|
|
63
|
+
* in libcrypto at first use over exactly one missing newline).
|
|
64
|
+
*/
|
|
65
|
+
normalizeTrailingNewline?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Capability probe (ticket 10): verify the credential's REAL
|
|
68
|
+
* permissions against the claimed tier. Core runs it at save time,
|
|
69
|
+
* after validation (credential files are on disk by then), and stores
|
|
70
|
+
* the result as a `probe` key beside the tier in the registry —
|
|
71
|
+
* surfaced by listAll / list_access / the admin UI. Receives the
|
|
72
|
+
* schema-validated, provider-processed fields. A probe must be
|
|
73
|
+
* READ-ONLY against the infrastructure (k8s runs a can-i matrix;
|
|
74
|
+
* ceph re-reads `auth get` caps). Providers that cannot probe (ssh —
|
|
75
|
+
* there is no read-only shell to test) omit the hook; their tiers
|
|
76
|
+
* stay unprobed. Probe failures degrade to 'unverifiable' — they
|
|
77
|
+
* never reject the write.
|
|
78
|
+
*/
|
|
79
|
+
probe?: (fields: Record<string, unknown>, tier: 'ro' | 'rw') => Promise<{
|
|
80
|
+
status: ProbeState['status'];
|
|
81
|
+
detail?: string;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
/** A resolved access profile: envelope fields plus the provider-processed type-specific fields. */
|
|
85
|
+
export interface AccessProfile {
|
|
86
|
+
kind: string;
|
|
87
|
+
/** The entry's registry key — its stable id (paths, mentions, grants). */
|
|
88
|
+
name: string;
|
|
89
|
+
/**
|
|
90
|
+
* The tier this resolve actually served ('rw' only via a broker grant).
|
|
91
|
+
* Consumers use it to label credential references, e.g. the shell-tool
|
|
92
|
+
* factory's <id@tier:field> display tokens.
|
|
93
|
+
*/
|
|
94
|
+
tier: 'ro' | 'rw';
|
|
95
|
+
/** Display label from the envelope's `name` field (editable, not an identity). */
|
|
96
|
+
displayName?: string;
|
|
97
|
+
description?: string;
|
|
98
|
+
environment?: string;
|
|
99
|
+
/** Type-specific fields after provider schema validation and process. */
|
|
100
|
+
fields: Record<string, unknown>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Envelope fields common to every entry (not provider-specific). The entry's
|
|
104
|
+
* registry key is its stable, human-readable **id** (used in file paths,
|
|
105
|
+
* mentions, tool calls, grants, and relation references); `name` is a
|
|
106
|
+
* freely editable display label — changing it touches nothing on disk.
|
|
107
|
+
*/
|
|
108
|
+
export interface EntryEnvelope {
|
|
109
|
+
/** Display name — modifiable, never referenced by id-based machinery. */
|
|
110
|
+
name?: string;
|
|
111
|
+
description?: string;
|
|
112
|
+
environment?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Capability-probe outcome for one tier (ticket 10): whether the stored
|
|
116
|
+
* credential's REAL permissions match its claimed tier, measured at save
|
|
117
|
+
* time. 'verified' = claims match reality; 'mismatch' = reality
|
|
118
|
+
* contradicts the tier (an admin credential sitting in the ro slot is
|
|
119
|
+
* the classic case); 'unverifiable' = the probe could not run (binary
|
|
120
|
+
* missing, cluster unreachable) — never a write rejection.
|
|
121
|
+
*/
|
|
122
|
+
export interface ProbeState {
|
|
123
|
+
status: 'verified' | 'mismatch' | 'unverifiable';
|
|
124
|
+
detail?: string;
|
|
125
|
+
/** ISO timestamp of the probe run. */
|
|
126
|
+
probedAt: string;
|
|
127
|
+
}
|
|
128
|
+
/** Validation status of one entry in one tier — `error` carries the zod reason, never field values. */
|
|
129
|
+
export interface AdminTierStatus {
|
|
130
|
+
ok: boolean;
|
|
131
|
+
error?: string;
|
|
132
|
+
/** Capability-probe outcome recorded at save time, when the provider probes. */
|
|
133
|
+
probe?: ProbeState;
|
|
134
|
+
}
|
|
135
|
+
/** One entry in the merged admin view: envelope + per-tier validation status, never fields. */
|
|
136
|
+
export interface AdminEntry {
|
|
137
|
+
kind: string;
|
|
138
|
+
name: string;
|
|
139
|
+
envelope: EntryEnvelope;
|
|
140
|
+
tiers: {
|
|
141
|
+
ro: AdminTierStatus;
|
|
142
|
+
rw: AdminTierStatus;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** One registered credential kind: its JSON Schema (from `zod.toJSONSchema`) and optional field docs. */
|
|
146
|
+
export interface KindDescriptor {
|
|
147
|
+
kind: string;
|
|
148
|
+
jsonSchema: Record<string, unknown>;
|
|
149
|
+
fieldsDoc?: string;
|
|
150
|
+
/** Field names that are file paths (content is managed by the admin UI). */
|
|
151
|
+
fileFields?: string[];
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Minimal caller-agent identity consulted by the broker. dsh's `Agent` (whose
|
|
155
|
+
* `id` is the session id) satisfies this structurally; core takes a narrow
|
|
156
|
+
* dependency on purpose — the broker is a pure decision function and never
|
|
157
|
+
* touches the rest of the agent.
|
|
158
|
+
*/
|
|
159
|
+
export interface AccessAgent {
|
|
160
|
+
/** Session id (`exec.agent.id`). Grants are keyed by this. */
|
|
161
|
+
readonly id: string;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* A broker's decision for one resolve call.
|
|
165
|
+
* - `'ro'` — serve the profile's `ro` tier from the registry
|
|
166
|
+
* - `'rw'` — serve the profile's `rw` tier from the registry
|
|
167
|
+
* - `{ deny }` — refuse; core throws the broker's message verbatim (the
|
|
168
|
+
* broker owns the guidance, e.g. pointing at `request_access`).
|
|
169
|
+
*/
|
|
170
|
+
export type AccessBrokerDecision = 'ro' | 'rw' | {
|
|
171
|
+
deny: string;
|
|
172
|
+
};
|
|
173
|
+
/**
|
|
174
|
+
* The pure decision function a gate registers. Receives only kind, profile
|
|
175
|
+
* name, and the caller agent — never credential fields. Once a broker is
|
|
176
|
+
* registered, resolve consults it on EVERY call; `agent` is `undefined` for
|
|
177
|
+
* system-internal calls, and the no-agent ruling belongs to the broker (core
|
|
178
|
+
* does not answer policy on its behalf). Without a registered broker, resolve
|
|
179
|
+
* is unchanged from the broker-less behavior (ro).
|
|
180
|
+
*/
|
|
181
|
+
export type AccessBroker = (kind: string, name: string, agent: AccessAgent | undefined) => AccessBrokerDecision;
|
|
182
|
+
/** The ops access handle exposed via ctx.get('opsAccess'). */
|
|
183
|
+
export interface OpsAccess {
|
|
184
|
+
/** Register a credential-kind provider. Throws if the kind is already registered. Returns a disposer. */
|
|
185
|
+
register(provider: AccessProvider): () => void;
|
|
186
|
+
/**
|
|
187
|
+
* Register an access broker (the gate). At most one broker is active; a later
|
|
188
|
+
* registration replaces an earlier one. Returns a disposer. Without a
|
|
189
|
+
* registered broker, resolve is unchanged from the broker-less behavior.
|
|
190
|
+
*/
|
|
191
|
+
registerBroker(broker: AccessBroker): () => void;
|
|
192
|
+
/**
|
|
193
|
+
* Whether resolving this profile from the given tier would succeed right
|
|
194
|
+
* now: the entry exists AND passes the provider schema. Never returns
|
|
195
|
+
* fields, never consults the broker — the gate uses it to reject
|
|
196
|
+
* undeliverable requests BEFORE bothering the human approver.
|
|
197
|
+
*
|
|
198
|
+
* Returns `{ ok: true }` when the entry exists and validates. Returns
|
|
199
|
+
* `{ ok: false, error }` when the entry exists but fails provider-schema
|
|
200
|
+
* validation — `error` is built from the zod issue paths and messages
|
|
201
|
+
* (never raw field values). Returns `{ ok: false }` (no `error`) for
|
|
202
|
+
* structural failures: unknown kind, missing file, unparseable file, or
|
|
203
|
+
* missing entry.
|
|
204
|
+
*/
|
|
205
|
+
canResolve(kind: string, name: string, tier: 'ro' | 'rw'): Promise<AdminTierStatus>;
|
|
206
|
+
/**
|
|
207
|
+
* Resolve one profile by kind and name. Throws on unknown kind, unknown
|
|
208
|
+
* name, or invalid entry. When a broker is registered it is consulted on
|
|
209
|
+
* every call — including calls without an `agent` (the broker owns the
|
|
210
|
+
* no-agent ruling) — and decides whether the rw profile is served. Without
|
|
211
|
+
* a broker the ro profile (from `registryFile`) is served, byte-for-byte
|
|
212
|
+
* as before.
|
|
213
|
+
*/
|
|
214
|
+
resolve(kind: string, name: string, agent?: AccessAgent): Promise<AccessProfile>;
|
|
215
|
+
/** List all profiles across all registered kinds. Sections without a registered provider are skipped. */
|
|
216
|
+
list(): Promise<AccessProfile[]>;
|
|
217
|
+
/**
|
|
218
|
+
* Write (upsert) one tier of one entry in the registry. Reads the file,
|
|
219
|
+
* sets entry[tier] to the given fields, validates the tier via the provider
|
|
220
|
+
* schema (buildProfile), and writes back. A validation failure throws and
|
|
221
|
+
* leaves the file untouched. Creates the file if it does not exist.
|
|
222
|
+
* The envelope is applied field-by-field: omitted fields are preserved,
|
|
223
|
+
* empty-string fields are deleted, so updating one tier never clobbers
|
|
224
|
+
* the existing envelope but the operator can still clear a field.
|
|
225
|
+
*/
|
|
226
|
+
writeEntry(kind: string, name: string, tier: 'ro' | 'rw', fields: Record<string, unknown>, envelope?: EntryEnvelope): Promise<void>;
|
|
227
|
+
/**
|
|
228
|
+
* Delete one tier of one entry from the registry. Returns true when the
|
|
229
|
+
* tier was deleted, false when the file, entry, or tier did not exist.
|
|
230
|
+
* When the entry's last tier is removed the whole entry is dropped, and
|
|
231
|
+
* the tier's managed credential files are removed with it.
|
|
232
|
+
*/
|
|
233
|
+
deleteEntry(kind: string, name: string, tier: 'ro' | 'rw'): Promise<boolean>;
|
|
234
|
+
/**
|
|
235
|
+
* List all entries across both tiers, merged by kind/name. Each entry
|
|
236
|
+
* carries its envelope (from whichever tier has it) and per-tier validation
|
|
237
|
+
* status via canResolve — never fields.
|
|
238
|
+
*/
|
|
239
|
+
listAll(): Promise<AdminEntry[]>;
|
|
240
|
+
/**
|
|
241
|
+
* Read back one entry's NON-file fields and envelope for editing. Returns
|
|
242
|
+
* null when the entry or file does not exist. File fields (credential
|
|
243
|
+
* content) are write-only after save: never returned, not even as paths —
|
|
244
|
+
* only their set status in `fileFields`. The caller (the admin UI) is a
|
|
245
|
+
* human operator, not the agent — non-file values are connection params,
|
|
246
|
+
* never secret material.
|
|
247
|
+
*/
|
|
248
|
+
getEntry(kind: string, name: string, tier: 'ro' | 'rw'): Promise<{
|
|
249
|
+
fields: Record<string, unknown>;
|
|
250
|
+
fileFields: Record<string, boolean>;
|
|
251
|
+
displayName?: string;
|
|
252
|
+
description?: string;
|
|
253
|
+
environment?: string;
|
|
254
|
+
} | null>;
|
|
255
|
+
/**
|
|
256
|
+
* List all registered credential kinds with their JSON Schema (serialized
|
|
257
|
+
* via `zod.toJSONSchema(provider.schema)`) and optional field docs.
|
|
258
|
+
* Unregistered kinds do not appear.
|
|
259
|
+
*/
|
|
260
|
+
listKinds(): KindDescriptor[];
|
|
261
|
+
/**
|
|
262
|
+
* The registry management doc: file location, format, envelope fields, and
|
|
263
|
+
* every registered kind's field doc. Progressive disclosure — the agent
|
|
264
|
+
* pulls this when it needs to edit the registry; nothing sits in the
|
|
265
|
+
* system prompt.
|
|
266
|
+
*/
|
|
267
|
+
help(): string;
|
|
268
|
+
}
|
package/lib/types.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type definitions for the ops-access capability seam.
|
|
3
|
+
*
|
|
4
|
+
* Types only — every runtime value (the `Config` schema, `expandHome`,
|
|
5
|
+
* `registerAccessProvider`/`registerAccessBroker`, the plugin itself) lives
|
|
6
|
+
* in index.ts.
|
|
7
|
+
*
|
|
8
|
+
* @module @elinpf/dsh-ops-access/types
|
|
9
|
+
*/
|
|
10
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elinpf/dsh-ops-access",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ops access capability seam — owns the YAML credential registry and exposes ctx.opsAccess (resolve/list/register) to provider plugins.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"lib/index.js",
|
|
10
|
+
"lib/invariant.js",
|
|
11
|
+
"lib/types.js",
|
|
12
|
+
"lib/mention.js",
|
|
13
|
+
"lib/**/*.d.ts",
|
|
14
|
+
"cordis.patch.yml"
|
|
15
|
+
],
|
|
16
|
+
"dsh": {
|
|
17
|
+
"bundle": {
|
|
18
|
+
"patch": "./cordis.patch.yml"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
|
|
23
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
24
|
+
"yaml": "^2.7.0",
|
|
25
|
+
"zod": "^4.4.3"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
29
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.1",
|
|
30
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.8"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@deepseek-ai/dsh-attachment": "0.0.1-rc.5",
|
|
34
|
+
"@deepseek-ai/dsh-brand": "0.0.1-rc.5",
|
|
35
|
+
"@deepseek-ai/dsh-invariants": "0.0.1-rc.5",
|
|
36
|
+
"@deepseek-ai/dsh-scope": "0.0.1-rc.5",
|
|
37
|
+
"@deepseek-ai/dsh-timeout": "0.0.1-rc.5",
|
|
38
|
+
"@deepseek-ai/cordis": "4.0.1",
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"typescript": "^5.4.0",
|
|
41
|
+
"vitest": "^4.1.11",
|
|
42
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
|
|
43
|
+
"@deepseek-ai/dsh-tools": "0.0.1-rc.1"
|
|
44
|
+
},
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"exports": {
|
|
47
|
+
".": {
|
|
48
|
+
"types": "./lib/index.d.ts",
|
|
49
|
+
"default": "./lib/index.js"
|
|
50
|
+
},
|
|
51
|
+
"./invariant": {
|
|
52
|
+
"types": "./lib/invariant.d.ts",
|
|
53
|
+
"default": "./lib/invariant.js"
|
|
54
|
+
},
|
|
55
|
+
"./types": {
|
|
56
|
+
"types": "./lib/types.d.ts",
|
|
57
|
+
"default": "./lib/types.js"
|
|
58
|
+
},
|
|
59
|
+
"./mention": {
|
|
60
|
+
"types": "./lib/mention.d.ts",
|
|
61
|
+
"default": "./lib/mention.js"
|
|
62
|
+
},
|
|
63
|
+
"./package.json": "./package.json"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
},
|
|
68
|
+
"scripts": {
|
|
69
|
+
"build": "tsc",
|
|
70
|
+
"typecheck": "tsc --noEmit",
|
|
71
|
+
"test": "vitest run"
|
|
72
|
+
}
|
|
73
|
+
}
|