@elinpf/dsh-ops-access 0.1.5 → 0.1.6
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/lib/index.js +37 -4
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
* @module @elinpf/dsh-ops-access
|
|
38
38
|
*/
|
|
39
39
|
import { readFile, writeFile, mkdir, rm, rmdir } from 'node:fs/promises';
|
|
40
|
+
import { resolve } from 'node:path';
|
|
40
41
|
import os from 'node:os';
|
|
41
42
|
import z from '@deepseek-ai/schemastery';
|
|
42
43
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
@@ -244,6 +245,17 @@ function assertValidProfileName(profileName) {
|
|
|
244
245
|
throw new Error(`ops-access: invalid profile name "${profileName}" — must start with a letter or digit and contain only letters, digits, '.', '_', '-', '@'`);
|
|
245
246
|
}
|
|
246
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* Whether a file-field value names a file instead of carrying the content:
|
|
250
|
+
* a single line starting with `/`, `~/`, `./`, or `../`. Real file-field
|
|
251
|
+
* content (kubeconfig, ceph.conf, keyring, private key) is always multi-line
|
|
252
|
+
* structured text, so a path-shaped single line is unambiguous.
|
|
253
|
+
* @param content - a file-field value already known to be a non-empty string.
|
|
254
|
+
* @returns true when the value should be read from disk as a file path.
|
|
255
|
+
*/
|
|
256
|
+
function looksLikeFilePath(content) {
|
|
257
|
+
return !content.includes('\n') && /^(?:\/|~\/|\.{1,2}\/)/.test(content);
|
|
258
|
+
}
|
|
247
259
|
async function writeContentFiles(credentialsDir, kind, profileName, tier, fileFields, contentFiles, entryFields, provider) {
|
|
248
260
|
const allowed = new Set(fileFields);
|
|
249
261
|
const written = [];
|
|
@@ -258,9 +270,29 @@ async function writeContentFiles(credentialsDir, kind, profileName, tier, fileFi
|
|
|
258
270
|
if (!allowed.has(fieldName)) {
|
|
259
271
|
throw new Error(`ops-access: "${fieldName}" is not a declared file field for kind "${kind}" (declared: ${fileFields.join(', ') || '(none)'})`);
|
|
260
272
|
}
|
|
273
|
+
// A single-line value starting with a path prefix names a file to read
|
|
274
|
+
// instead of the content itself — the credential then never round-trips
|
|
275
|
+
// through the model request or the admin form. File-field content
|
|
276
|
+
// (kubeconfig, ceph.conf, keyring, private key) is always multi-line
|
|
277
|
+
// structured text, so the two never collide. A path with no file behind
|
|
278
|
+
// it fails loud: the commonest mistake is passing a path where content
|
|
279
|
+
// is expected.
|
|
280
|
+
let resolved = content;
|
|
281
|
+
if (looksLikeFilePath(content)) {
|
|
282
|
+
const source = content.startsWith('~/')
|
|
283
|
+
? (process.env.HOME ?? os.homedir()) + content.slice(1)
|
|
284
|
+
: resolve(content);
|
|
285
|
+
try {
|
|
286
|
+
resolved = await readFile(source, 'utf8');
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
const code = error.code ?? 'unreadable';
|
|
290
|
+
throw new Error(`ops-access: "${fieldName}" looks like a file path, but no readable file at ${source} (${code}) — pass the full file CONTENT, or a path to an existing file`);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
261
293
|
// Provider-declared write-time normalization runs FIRST — validator
|
|
262
294
|
// and disk both see the normalized bytes.
|
|
263
|
-
const normalized = provider?.normalizeTrailingNewline ?
|
|
295
|
+
const normalized = provider?.normalizeTrailingNewline ? resolved.replace(/[\r\n]+$/, '') + '\n' : resolved;
|
|
264
296
|
// Save-time content validation (provider hook, possibly async — ssh
|
|
265
297
|
// runs ssh-keygen): reject corrupt pastes BEFORE anything lands on disk.
|
|
266
298
|
const problem = await provider?.validateContent?.(fieldName, normalized);
|
|
@@ -513,7 +545,8 @@ export function apply(ctx, config) {
|
|
|
513
545
|
}
|
|
514
546
|
lines.push('');
|
|
515
547
|
lines.push('Agents register ro tiers with the register_access tool — rw tiers stay human-managed via the admin UI.');
|
|
516
|
-
lines.push('
|
|
548
|
+
lines.push('Registering: pass the full file CONTENT for file fields, or a single-line path to an existing readable file (read server-side, content never passes through the model). Multi-line pastes are always treated as content.');
|
|
549
|
+
lines.push('In the REGISTRY itself, file fields carry the managed file paths — secrets never go inline, so logs and model context never contain secret material.');
|
|
517
550
|
return lines.join('\n');
|
|
518
551
|
},
|
|
519
552
|
async writeEntry(kind, profileName, tier, fields, envelope) {
|
|
@@ -711,10 +744,10 @@ export function apply(ctx, config) {
|
|
|
711
744
|
// session event log, so every registration is reconstructable.
|
|
712
745
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
713
746
|
name: 'register_access',
|
|
714
|
-
description: 'Register or overwrite the read-only (ro) credential tier of an access profile — typically a credential you derived from the rw tier (a read-only ServiceAccount token, a read-only cephx keyring, a dedicated SSH key). The rw tier is human-managed via the admin UI; this tool writes ro only. File fields (kubeconfig, conf, keyring, key) take full file CONTENT, stored to a managed path automatically;
|
|
747
|
+
description: 'Register or overwrite the read-only (ro) credential tier of an access profile — typically a credential you derived from the rw tier (a read-only ServiceAccount token, a read-only cephx keyring, a dedicated SSH key). The rw tier is human-managed via the admin UI; this tool writes ro only. File fields (kubeconfig, conf, keyring, key) take the full file CONTENT, stored to a managed path automatically; a path to an existing readable file also works and is read server-side, so the content never needs to pass through this call. Other fields are inline values. Run list_access with help: true for per-kind field docs and derivation recipes.',
|
|
715
748
|
parameters: {
|
|
716
749
|
profile: { type: 'string', required: true, description: '"kind/id", e.g. "k8s/prod". The entry is created when it does not exist yet.' },
|
|
717
|
-
fields: { type: 'object', additionalProperties: true, required: true, description: 'The ro tier field values for this kind. File fields take full
|
|
750
|
+
fields: { type: 'object', additionalProperties: true, required: true, description: 'The ro tier field values for this kind. File fields (kubeconfig, conf, keyring, key) take the full file CONTENT — or a single-line path to an existing readable file, which is read server-side. Multi-line pastes are always treated as content.' },
|
|
718
751
|
description: { type: 'string', description: 'Optional envelope description (empty string clears it).' },
|
|
719
752
|
environment: { type: 'string', description: 'Optional envelope environment label (empty string clears it).' },
|
|
720
753
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@elinpf/dsh-ops-access",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Ops access capability seam — owns the YAML credential registry and exposes ctx.opsAccess (resolve/list/register) to provider plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|