@n8r/surface 0.0.0-stage → 0.1.0-experimental.5
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 +202 -0
- package/LICENSE-DOCS +395 -0
- package/LICENSING.md +36 -0
- package/README.md +207 -2
- package/dist/activity.d.ts +16 -0
- package/dist/activity.js +44 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +412 -0
- package/dist/codec.d.ts +6 -0
- package/dist/codec.js +56 -0
- package/dist/codex.d.ts +72 -0
- package/dist/codex.js +526 -0
- package/dist/contracts.d.ts +235 -0
- package/dist/contracts.js +1 -0
- package/dist/cyclonedx-schemas/bom-1.7.schema.json +6700 -0
- package/dist/cyclonedx-schemas/cryptography-defs.schema.json +576 -0
- package/dist/cyclonedx-schemas/jsf-0.82.schema.json +240 -0
- package/dist/cyclonedx-schemas/spdx.schema.json +786 -0
- package/dist/cyclonedx-validation.d.ts +1 -0
- package/dist/cyclonedx-validation.js +45 -0
- package/dist/html.d.ts +5 -0
- package/dist/html.js +74 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/notes.d.ts +52 -0
- package/dist/notes.js +184 -0
- package/dist/portable.d.ts +10 -0
- package/dist/portable.js +71 -0
- package/dist/recent.d.ts +9 -0
- package/dist/recent.js +71 -0
- package/dist/revisions.d.ts +2 -0
- package/dist/revisions.js +60 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +100 -0
- package/dist/setup.d.ts +9 -0
- package/dist/setup.js +161 -0
- package/dist/skill-artifacts-cli.d.ts +1 -0
- package/dist/skill-artifacts-cli.js +13 -0
- package/dist/skill-artifacts.d.ts +15 -0
- package/dist/skill-artifacts.js +184 -0
- package/dist/skill-cyclonedx.d.ts +26 -0
- package/dist/skill-cyclonedx.js +214 -0
- package/dist/skill-presentation.d.ts +10 -0
- package/dist/skill-presentation.js +108 -0
- package/dist/skill-surface-cli.d.ts +1 -0
- package/dist/skill-surface-cli.js +37 -0
- package/dist/skill-surface-store.d.ts +7 -0
- package/dist/skill-surface-store.js +126 -0
- package/dist/source-tools.d.ts +102 -0
- package/dist/source-tools.js +201 -0
- package/dist/startup.d.ts +7 -0
- package/dist/startup.js +74 -0
- package/dist/surface-bom.d.ts +73 -0
- package/dist/surface-bom.js +139 -0
- package/dist/surface-draft.d.ts +22 -0
- package/dist/surface-draft.js +112 -0
- package/dist/surface-instructions.d.ts +3 -0
- package/dist/surface-instructions.js +32 -0
- package/dist/surface-overview.d.ts +15 -0
- package/dist/surface-overview.js +86 -0
- package/dist/surface-store.d.ts +34 -0
- package/dist/surface-store.js +213 -0
- package/dist/surface-view.d.ts +3 -0
- package/dist/surface-view.js +50 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +482 -0
- package/docs/n8r-surface/README.md +133 -0
- package/docs/n8r-surface/cli-bundles.md +72 -0
- package/docs/n8r-surface/codex-research-transcript.md +51 -0
- package/docs/n8r-surface/codex-research-verification.md +176 -0
- package/docs/n8r-surface/command-outcomes.md +711 -0
- package/docs/n8r-surface/contracts.md +220 -0
- package/docs/n8r-surface/critical-path.md +184 -0
- package/docs/n8r-surface/design.md +289 -0
- package/docs/n8r-surface/first-use-access.md +143 -0
- package/docs/n8r-surface/first-use-overview.md +262 -0
- package/docs/n8r-surface/getting-started.md +74 -0
- package/docs/n8r-surface/guided-journey-verification.md +137 -0
- package/docs/n8r-surface/host-contract.md +176 -0
- package/docs/n8r-surface/implementation-decisions.md +96 -0
- package/docs/n8r-surface/instance-first-research.md +209 -0
- package/docs/n8r-surface/npm-distribution.md +107 -0
- package/docs/n8r-surface/npm-release.md +315 -0
- package/docs/n8r-surface/product-direction.md +327 -0
- package/docs/n8r-surface/recreation-handoff.md +117 -0
- package/docs/n8r-surface/researcher-discovery.md +157 -0
- package/docs/n8r-surface/researcher-prepared-check.md +117 -0
- package/docs/n8r-surface/researcher-reconciliation-trial.md +149 -0
- package/docs/n8r-surface/researcher-repository-check.md +95 -0
- package/docs/n8r-surface/researcher-skeleton-check.md +84 -0
- package/docs/n8r-surface/researcher-skill-verification.md +725 -0
- package/docs/n8r-surface/researcher-skill.md +309 -0
- package/docs/n8r-surface/researcher-vault.md +134 -0
- package/docs/n8r-surface/save-reopen-transcript.md +211 -0
- package/docs/n8r-surface/save-reopen-verification.md +152 -0
- package/docs/n8r-surface/security-assurance-strategy.md +267 -0
- package/docs/n8r-surface/skill-bom-verification.md +142 -0
- package/docs/n8r-surface/skill-cyclonedx-verification.md +144 -0
- package/docs/n8r-surface/skill-cyclonedx.md +85 -0
- package/docs/n8r-surface/skill-manual.md +129 -0
- package/docs/n8r-surface/skill-surface-artifacts.md +234 -0
- package/docs/n8r-surface/skill-usefulness-verification.md +135 -0
- package/docs/n8r-surface/skill-workspace-automation.md +359 -0
- package/docs/n8r-surface/startup-human-check.md +172 -0
- package/docs/n8r-surface/startup-verification.md +161 -0
- package/docs/n8r-surface/startup.md +151 -0
- package/docs/n8r-surface/surface-description-contract.md +224 -0
- package/docs/n8r-surface/surface-descriptions.md +173 -0
- package/docs/n8r-surface/surface-relationships.md +231 -0
- package/docs/n8r-surface/surface-revisions-transcript.md +552 -0
- package/docs/n8r-surface/surface-revisions-verification.md +161 -0
- package/docs/n8r-surface/tester-candidate.md +158 -0
- package/docs/n8r-surface/vault-setup-verification.md +210 -0
- package/docs/n8r-surface/verification.md +117 -0
- package/docs/n8r-surface/versioning.md +172 -0
- package/docs/n8r-surface/workspace-human-check.md +101 -0
- package/docs/n8r-surface/workspace-installation.md +68 -0
- package/docs/n8r-surface/workspace-verification.md +350 -0
- package/docs/n8r-surface/workspace.md +83 -0
- package/examples/researcher/README.md +100 -0
- package/examples/researcher/composed.surface.json +430 -0
- package/examples/researcher/composed.surface.yaml +283 -0
- package/examples/researcher/cyclonedx/configured/README.md +7 -0
- package/examples/researcher/cyclonedx/configured/manifest.json +10 -0
- package/examples/researcher/cyclonedx/configured/mapping.json +323 -0
- package/examples/researcher/cyclonedx/configured/surface-records.json +1275 -0
- package/examples/researcher/cyclonedx/configured/surface.cdx.json +79 -0
- package/examples/researcher/descriptions/README.md +29 -0
- package/examples/researcher/descriptions/bound-r1.json +141 -0
- package/examples/researcher/descriptions/bound-r2.json +153 -0
- package/examples/researcher/descriptions/capability.json +167 -0
- package/examples/researcher/descriptions/research-r1.json +153 -0
- package/examples/researcher/descriptions/research-r2.json +157 -0
- package/examples/researcher/descriptions/sources.md +24 -0
- package/examples/researcher/harbor-r1.research.html +193 -0
- package/examples/researcher/harbor-r2.research.html +226 -0
- package/examples/researcher/lumen-manual-edit.pending.html +271 -0
- package/examples/researcher/lumen-r1.research.html +271 -0
- package/examples/researcher/lumen-r2.research.html +280 -0
- package/examples/researcher/relationships/README.md +34 -0
- package/examples/researcher/relationships/bound-r1.json +346 -0
- package/examples/researcher/relationships/bound-r2.json +358 -0
- package/examples/researcher/relationships/capability.json +325 -0
- package/examples/researcher/relationships/config/assistant.env.example +4 -0
- package/examples/researcher/relationships/research-r1.json +300 -0
- package/examples/researcher/relationships/research-r2.json +304 -0
- package/examples/researcher/relationships/sources.md +38 -0
- package/examples/researcher/sources/harbor-api.md +16 -0
- package/examples/researcher/sources/harbor-auth.md +8 -0
- package/examples/researcher/sources/lumen-guide.md +28 -0
- package/examples/researcher/sources/lumen-settings.md +9 -0
- package/examples/researcher/surface-bom/README.md +17 -0
- package/examples/researcher/surface-bom/initial.cdx.json +188 -0
- package/package.json +45 -4
- package/plugins/n8r-surface/.codex-plugin/plugin.json +31 -0
- package/plugins/n8r-surface/LICENSE +202 -0
- package/plugins/n8r-surface/LICENSE-DOCS +395 -0
- package/plugins/n8r-surface/LICENSING.md +39 -0
- package/plugins/n8r-surface/assets/icon.svg +5 -0
- package/plugins/n8r-surface/plugin.json +27 -0
- package/plugins/n8r-surface/skills/research/SKILL.md +163 -0
- package/plugins/n8r-surface/skills/research/agents/openai.yaml +4 -0
- package/plugins/n8r-surface/skills/research/references/artifacts.md +152 -0
- package/plugins/n8r-surface/skills/research/references/cyclonedx.md +207 -0
- package/plugins/n8r-surface/skills/research/references/discovery-map.md +178 -0
- package/plugins/n8r-surface/skills/research/references/examples/bound-r1.json +346 -0
- package/plugins/n8r-surface/skills/research/references/examples/capability.json +325 -0
- package/plugins/n8r-surface/skills/research/references/examples/research-r1.json +300 -0
- package/plugins/n8r-surface/skills/research/references/examples/surface.cdx.json +188 -0
- package/plugins/n8r-surface/skills/research/references/first-use-access.md +97 -0
- package/plugins/n8r-surface/skills/research/references/first-use-overview.md +101 -0
- package/plugins/n8r-surface/skills/research/references/instance-first.md +125 -0
- package/plugins/n8r-surface/skills/research/references/model-meanings.md +55 -0
- package/plugins/n8r-surface/skills/research/references/skeleton-surface.md +105 -0
- package/plugins/n8r-surface/skills/research/references/working-notes.md +121 -0
- package/plugins/n8r-surface/skills/research/references/workspace.md +89 -0
- package/plugins/n8r-surface/skills/research/scripts/THIRD-PARTY-NOTICES.txt +273 -0
- package/plugins/n8r-surface/skills/research/scripts/artifacts.mjs +8348 -0
- package/plugins/n8r-surface/skills/research/scripts/cyclonedx.mjs +28113 -0
- package/plugins/n8r-surface/skills/research/scripts/surface.mjs +27800 -0
- package/plugins/n8r-surface/skills/research/scripts/workspace.mjs +120 -0
package/dist/settings.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { constants, openSync, closeSync, fstatSync, readSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
/** Escape terminal controls and line/bidi separators in untrusted labels. */
|
|
5
|
+
export function line(value) {
|
|
6
|
+
return value.replace(/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu, c => `\\u${c.codePointAt(0).toString(16).padStart(4, '0')}`);
|
|
7
|
+
}
|
|
8
|
+
export class InspectionError extends Error {
|
|
9
|
+
}
|
|
10
|
+
export function problem(error) {
|
|
11
|
+
if (error instanceof InspectionError)
|
|
12
|
+
return error.message;
|
|
13
|
+
const code = error?.code;
|
|
14
|
+
return typeof code === 'string' && /^[A-Z0-9_]+$/.test(code) ? code : 'Read failed';
|
|
15
|
+
}
|
|
16
|
+
/** Do not block on special files, follow entry symlinks or read unbounded data. */
|
|
17
|
+
export function readBytes(path, limit) {
|
|
18
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
19
|
+
try {
|
|
20
|
+
const stat = fstatSync(fd);
|
|
21
|
+
if (!stat.isFile())
|
|
22
|
+
throw new InspectionError('Not a regular file');
|
|
23
|
+
if (stat.size > limit)
|
|
24
|
+
throw new InspectionError(`File exceeds ${limit} bytes`);
|
|
25
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
26
|
+
let bytes = 0;
|
|
27
|
+
while (bytes < buffer.length) {
|
|
28
|
+
const count = readSync(fd, buffer, bytes, buffer.length - bytes, null);
|
|
29
|
+
if (!count)
|
|
30
|
+
break;
|
|
31
|
+
bytes += count;
|
|
32
|
+
}
|
|
33
|
+
if (bytes > limit)
|
|
34
|
+
throw new InspectionError(`File exceeds ${limit} bytes`);
|
|
35
|
+
return buffer.subarray(0, bytes);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function object(value) {
|
|
42
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
43
|
+
}
|
|
44
|
+
function settings(text, home, file) {
|
|
45
|
+
let data = { version: 1 };
|
|
46
|
+
if (text !== undefined) {
|
|
47
|
+
try {
|
|
48
|
+
data = JSON.parse(text);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
throw new InspectionError('Invalid JSON; repair the existing file');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!object(data) || data.version !== 1)
|
|
55
|
+
throw new InspectionError('Expected settings object with version 1');
|
|
56
|
+
const path = localPath(data.artefactRoot === undefined ? '~/n8r/artefacts' : data.artefactRoot, home, dirname(file));
|
|
57
|
+
const vault = data.vaultRoot === undefined ? undefined : localPath(data.vaultRoot, home, dirname(file));
|
|
58
|
+
const format = data.surfaceFormat === undefined ? 'json' : data.surfaceFormat;
|
|
59
|
+
if (format !== 'json' && format !== 'yaml')
|
|
60
|
+
throw new InspectionError('surfaceFormat must be json or yaml');
|
|
61
|
+
for (const key of ['runner', 'lab']) {
|
|
62
|
+
if (data[key] !== undefined && data[key] !== null && !object(data[key]))
|
|
63
|
+
throw new InspectionError(`${key} must be an object or null`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
home, file, original: undefined, data, vault, root: path,
|
|
67
|
+
format, defaultRoot: data.artefactRoot === undefined, defaultFormat: data.surfaceFormat === undefined,
|
|
68
|
+
runner: object(data.runner), lab: object(data.lab)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function localPath(value, home, base) {
|
|
72
|
+
if (typeof value !== 'string' || !value.trim() || /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/u.test(value) || /^[a-z][a-z0-9+.-]*:/i.test(value) || (value.startsWith('~') && !value.startsWith('~/'))) {
|
|
73
|
+
throw new InspectionError('Expected a local path, optionally starting with ~/');
|
|
74
|
+
}
|
|
75
|
+
return value.startsWith('~/') ? resolve(home, value.slice(2)) : resolve(base, value);
|
|
76
|
+
}
|
|
77
|
+
export function readLocal(path, limit) {
|
|
78
|
+
return readBytes(path, limit).toString('utf8');
|
|
79
|
+
}
|
|
80
|
+
export function loadSettings(home = homedir()) {
|
|
81
|
+
const file = join(home, '.n8r/n8r-surface.json');
|
|
82
|
+
let original;
|
|
83
|
+
try {
|
|
84
|
+
original = readBytes(file, 64 * 1024);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code !== 'ENOENT')
|
|
88
|
+
throw error;
|
|
89
|
+
// A dangling symlink is not an absent settings file.
|
|
90
|
+
try {
|
|
91
|
+
lstatSync(file);
|
|
92
|
+
throw new InspectionError('Settings is not a regular file');
|
|
93
|
+
}
|
|
94
|
+
catch (entryError) {
|
|
95
|
+
if (entryError.code !== 'ENOENT')
|
|
96
|
+
throw entryError;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { ...settings(original?.toString('utf8'), home, file), original };
|
|
100
|
+
}
|
package/dist/setup.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Settings } from './settings.js';
|
|
2
|
+
export declare function chooseVault(value: string, home: string, cwd: string): string;
|
|
3
|
+
/** Read-only validation: missing folders need consent, occupied paths are errors. */
|
|
4
|
+
export declare function inspectVault(root: string): {
|
|
5
|
+
root: string;
|
|
6
|
+
missing: boolean;
|
|
7
|
+
};
|
|
8
|
+
/** Called only after explicit acceptance. See startup.md for the writer boundary. */
|
|
9
|
+
export declare function saveSetup(snapshot: Settings, selected: string, cwd: string): string;
|
package/dist/setup.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { dirname, join, relative, resolve, sep } from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { InspectionError, loadSettings, localPath } from './settings.js';
|
|
5
|
+
const children = ['research', 'artifacts', 'surfaces'];
|
|
6
|
+
/** Resolve existing ancestors as well as explicitly chosen root symlinks. */
|
|
7
|
+
function canonical(path) {
|
|
8
|
+
try {
|
|
9
|
+
return fs.realpathSync(path);
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code !== 'ENOENT')
|
|
13
|
+
throw error;
|
|
14
|
+
// Reject dangling symlinks rather than treating them as new directories.
|
|
15
|
+
try {
|
|
16
|
+
fs.lstatSync(path);
|
|
17
|
+
throw new InspectionError('Dangling symlink');
|
|
18
|
+
}
|
|
19
|
+
catch (entryError) {
|
|
20
|
+
if (entryError.code !== 'ENOENT')
|
|
21
|
+
throw entryError;
|
|
22
|
+
}
|
|
23
|
+
const parent = dirname(path);
|
|
24
|
+
if (parent === path)
|
|
25
|
+
throw error;
|
|
26
|
+
return join(canonical(parent), path.slice(parent.length));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function chooseVault(value, home, cwd) {
|
|
30
|
+
return localPath(canonical(localPath(value, home, cwd)), home, cwd);
|
|
31
|
+
}
|
|
32
|
+
/** Read-only validation: missing folders need consent, occupied paths are errors. */
|
|
33
|
+
export function inspectVault(root) {
|
|
34
|
+
root = canonical(root);
|
|
35
|
+
let missing = false;
|
|
36
|
+
for (const path of [root, ...children.map(child => join(root, child))]) {
|
|
37
|
+
try {
|
|
38
|
+
const stat = fs.lstatSync(path);
|
|
39
|
+
if (stat.isSymbolicLink())
|
|
40
|
+
throw new InspectionError('Vault children cannot be symlinks');
|
|
41
|
+
if (!stat.isDirectory())
|
|
42
|
+
throw new InspectionError('Vault path is not a directory');
|
|
43
|
+
fs.accessSync(path, fs.constants.R_OK | fs.constants.X_OK);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
if (error.code !== 'ENOENT')
|
|
47
|
+
throw error;
|
|
48
|
+
missing = true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { root, missing };
|
|
52
|
+
}
|
|
53
|
+
function inside(path, source) {
|
|
54
|
+
const part = relative(source, canonical(path));
|
|
55
|
+
return part === '' || (part !== '..' && !part.startsWith(`..${sep}`) && !part.startsWith(sep));
|
|
56
|
+
}
|
|
57
|
+
function unchanged(snapshot) {
|
|
58
|
+
const current = loadSettings(snapshot.home).original;
|
|
59
|
+
if (snapshot.original === undefined ? current !== undefined : current === undefined || !snapshot.original.equals(current)) {
|
|
60
|
+
throw new InspectionError('Settings changed; setup conflict. Restart and review the saved values.');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
/** Called only after explicit acceptance. See startup.md for the writer boundary. */
|
|
64
|
+
export function saveSetup(snapshot, selected, cwd) {
|
|
65
|
+
const vault = inspectVault(chooseVault(selected, snapshot.home, cwd)).root;
|
|
66
|
+
const source = canonical(resolve(cwd));
|
|
67
|
+
const file = snapshot.file;
|
|
68
|
+
for (const path of [dirname(file), file, vault, ...children.map(child => join(vault, child))]) {
|
|
69
|
+
if (inside(path, source))
|
|
70
|
+
throw new InspectionError('Setup would write inside the source; choose a separate vault or source directory.');
|
|
71
|
+
}
|
|
72
|
+
if (snapshot.vault !== undefined && canonical(snapshot.vault) !== vault)
|
|
73
|
+
throw new InspectionError('Configured vault cannot be changed by setup');
|
|
74
|
+
const legacy = canonical(snapshot.root);
|
|
75
|
+
for (const child of children) {
|
|
76
|
+
const path = join(vault, child);
|
|
77
|
+
if (canonical(path) !== legacy && inside(path, legacy)) {
|
|
78
|
+
throw new InspectionError('Vault folders would occupy the legacy artefact corpus; choose a separate vault.');
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// Keep unknown JSON member bytes, including numbers outside JS precision/range.
|
|
82
|
+
// The validated original is an object containing version, so a comma is valid.
|
|
83
|
+
let output = Buffer.alloc(0);
|
|
84
|
+
if (snapshot.vault === undefined) {
|
|
85
|
+
if (snapshot.original === undefined) {
|
|
86
|
+
output = Buffer.from(JSON.stringify({ version: 1, vaultRoot: vault }, null, 2) + '\n');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const end = snapshot.original.lastIndexOf(0x7d);
|
|
90
|
+
output = Buffer.concat([snapshot.original.subarray(0, end),
|
|
91
|
+
Buffer.from(`,\n "vaultRoot": ${JSON.stringify(vault)}\n`), snapshot.original.subarray(end)]);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (output.length > 64 * 1024)
|
|
95
|
+
throw new InspectionError('Updated settings exceeds 65536 bytes');
|
|
96
|
+
fs.mkdirSync(dirname(file), { recursive: true });
|
|
97
|
+
const lock = file + '.lock';
|
|
98
|
+
let lockFd;
|
|
99
|
+
try {
|
|
100
|
+
lockFd = fs.openSync(lock, 'wx', 0o600);
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
if (error.code === 'EEXIST')
|
|
104
|
+
throw new InspectionError('Setup lock exists; another setup may be in progress. See startup documentation for stale-lock recovery.');
|
|
105
|
+
throw error;
|
|
106
|
+
}
|
|
107
|
+
let temporary;
|
|
108
|
+
try {
|
|
109
|
+
unchanged(snapshot);
|
|
110
|
+
fs.mkdirSync(vault, { recursive: true });
|
|
111
|
+
for (const child of children) {
|
|
112
|
+
const path = join(vault, child);
|
|
113
|
+
try {
|
|
114
|
+
fs.mkdirSync(path);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (error.code !== 'EEXIST')
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
const stat = fs.lstatSync(path);
|
|
121
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
122
|
+
throw new InspectionError('Vault child is occupied or a symlink');
|
|
123
|
+
}
|
|
124
|
+
inspectVault(vault);
|
|
125
|
+
unchanged(snapshot);
|
|
126
|
+
if (snapshot.vault === undefined) {
|
|
127
|
+
const candidate = file + '.' + randomUUID() + '.tmp';
|
|
128
|
+
const fd = fs.openSync(candidate, 'wx', 0o600);
|
|
129
|
+
temporary = candidate;
|
|
130
|
+
try {
|
|
131
|
+
fs.writeFileSync(fd, output);
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
fs.closeSync(fd);
|
|
135
|
+
}
|
|
136
|
+
unchanged(snapshot);
|
|
137
|
+
if (snapshot.original === undefined)
|
|
138
|
+
fs.linkSync(temporary, file);
|
|
139
|
+
else
|
|
140
|
+
fs.renameSync(temporary, file);
|
|
141
|
+
}
|
|
142
|
+
return vault;
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
try {
|
|
146
|
+
if (temporary !== undefined) {
|
|
147
|
+
try {
|
|
148
|
+
fs.unlinkSync(temporary);
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
if (error.code !== 'ENOENT')
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
fs.closeSync(lockFd);
|
|
158
|
+
fs.unlinkSync(lock);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './skill-artifacts.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { resolve } from 'node:path';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { main } from './skill-artifacts.js';
|
|
4
|
+
export * from './skill-artifacts.js';
|
|
5
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
|
|
6
|
+
try {
|
|
7
|
+
main(process.argv.slice(2));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
console.error('Surface operation failed. Check input contracts, privacy, references, predecessor and destination; no success claimed.');
|
|
11
|
+
process.exitCode = 1;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Artefact, ResearchRecord, SurfaceGraph } from './contracts.js';
|
|
2
|
+
export interface Bundle {
|
|
3
|
+
research: ResearchRecord;
|
|
4
|
+
surface: SurfaceGraph;
|
|
5
|
+
dependencies: Artefact[];
|
|
6
|
+
}
|
|
7
|
+
/** Conservative defense in depth; host must omit sensitive values before staging. */
|
|
8
|
+
export declare function privacy(value: unknown): void;
|
|
9
|
+
export declare function checkBundle(input: unknown): Bundle;
|
|
10
|
+
export declare function readBundle(directory: string): Bundle;
|
|
11
|
+
export declare function verifyBundleSuccessor(parent: Bundle, b: Bundle): void;
|
|
12
|
+
export declare function saveBundle(input: unknown, destination: string, predecessor?: string): Bundle;
|
|
13
|
+
export declare function main(args: string[]): void;
|
|
14
|
+
/** Same canonical digest, with value-free diagnostics at the skill boundary. */
|
|
15
|
+
export declare function digest(value: unknown): string;
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/** Standalone installed-skill persistence. No CLI, runner, network or source reads. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { resolve, dirname, join } from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { decode, encode, decodeJsonValue } from './codec.js';
|
|
6
|
+
import { validateArtefact, verifyReferences } from './validate.js';
|
|
7
|
+
import { digest as artifactDigest, canonical } from './portable.js';
|
|
8
|
+
import { readResearchHtml, renderResearchHtml } from './html.js';
|
|
9
|
+
import { renderBrief, renderBundleHtml } from './skill-presentation.js';
|
|
10
|
+
import { verifySuccessor } from './revisions.js';
|
|
11
|
+
const members = ['research.html', 'surface.json', 'dependencies.json', 'brief.md'];
|
|
12
|
+
const hash = (s) => `sha256:${createHash('sha256').update(s).digest('hex')}`;
|
|
13
|
+
function fail(code) { throw new Error(`Surface operation refused: ${code}. No input values are included.`); }
|
|
14
|
+
function guarded(code, fn) { try {
|
|
15
|
+
return fn();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return fail(code);
|
|
19
|
+
} }
|
|
20
|
+
/** Conservative defense in depth; host must omit sensitive values before staging. */
|
|
21
|
+
export function privacy(value) {
|
|
22
|
+
const text = canonical(value);
|
|
23
|
+
if (/(?:https?|mongodb(?:\+srv)?|postgres(?:ql)?):\/\/|\b(?:\d{1,3}\.){3}\d{1,3}\b|\b(?:[a-f0-9]{1,4}:){2,}[a-f0-9:]+\b|(?:password|passwd|token|secret|username|api[_-]?key)\s*[=:]\s*[^\s"},]+|(?:\/Users\/|\/home\/|[A-Z]:\\\\)|[\w.+-]+@[\w.-]+\.[a-z]{2,}/i.test(text))
|
|
24
|
+
fail('privacy review required');
|
|
25
|
+
}
|
|
26
|
+
export function checkBundle(input) {
|
|
27
|
+
return guarded('bundle validation or privacy check', () => {
|
|
28
|
+
if (!input || typeof input !== 'object' || Object.keys(input).sort().join(',') !== 'dependencies,research,surface')
|
|
29
|
+
fail('bundle shape');
|
|
30
|
+
const b = input;
|
|
31
|
+
privacy(b);
|
|
32
|
+
const research = validateArtefact(b.research), surface = validateArtefact(b.surface);
|
|
33
|
+
if (research.contract !== 'n8r.research/0.1' || surface.contract !== 'n8r.surface/0.3' || !Array.isArray(b.dependencies) || b.dependencies.length > 100)
|
|
34
|
+
fail('contracts');
|
|
35
|
+
const context = [];
|
|
36
|
+
for (const a of [research, surface, ...b.dependencies.map(validateArtefact)]) {
|
|
37
|
+
if (Object.keys(a.extensions).length)
|
|
38
|
+
fail('opaque extensions unsupported by this host');
|
|
39
|
+
const old = context.find(x => x.id === a.id && x.revision.id === a.revision.id);
|
|
40
|
+
if (old && digest(old) !== digest(a))
|
|
41
|
+
fail('ambiguous revision');
|
|
42
|
+
if (!old)
|
|
43
|
+
context.push(a);
|
|
44
|
+
}
|
|
45
|
+
if (!surface.records.some(r => r.id === research.id && r.revision === research.revision.id && r.digest === digest(research)))
|
|
46
|
+
fail('unlinked research');
|
|
47
|
+
for (const a of context)
|
|
48
|
+
verifyReferences(a, context);
|
|
49
|
+
return { research, surface, dependencies: b.dependencies };
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
function directories(dir) {
|
|
53
|
+
for (let p = resolve(dir);; p = dirname(p)) {
|
|
54
|
+
if (fs.existsSync(p)) {
|
|
55
|
+
const s = fs.lstatSync(p);
|
|
56
|
+
if (s.isSymbolicLink() || !s.isDirectory())
|
|
57
|
+
fail('unsafe directory');
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
try {
|
|
61
|
+
fs.lstatSync(p);
|
|
62
|
+
fail('unsafe directory');
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
if (e.code !== 'ENOENT')
|
|
66
|
+
throw e;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (p === dirname(p))
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function read(path) {
|
|
74
|
+
const fd = fs.openSync(path, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
75
|
+
try {
|
|
76
|
+
const stat = fs.fstatSync(fd);
|
|
77
|
+
if (!stat.isFile() || stat.size > 8 * 1024 * 1024)
|
|
78
|
+
fail('file size/type');
|
|
79
|
+
return new TextDecoder('utf-8', { fatal: true }).decode(fs.readFileSync(fd));
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
fs.closeSync(fd);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const plain = (s) => s.replace(/[\r\n]/g, ' ').replace(/[<>\[\]`|]/g, '');
|
|
86
|
+
function brief(b) {
|
|
87
|
+
const s = b.surface, name = (id) => plain(s.nodes.find(n => n.id === id)?.label ?? id);
|
|
88
|
+
return `# ${plain(s.title)}\n\n${s.kind === 'bound' ? 'Bound' : 'Capability'} Surface · ${plain(s.revision.id)}\n\n${plain(s.scope)}\n\n` +
|
|
89
|
+
s.relationships.map(e => {
|
|
90
|
+
const state = 'binding' in e ? e.binding.state : e.state;
|
|
91
|
+
const target = 'binding' in e ? e.binding.claims.map(c => name(c.target)).join(', ') : 'supported possibility';
|
|
92
|
+
const detail = (d) => `${d.state}: ${d.claims.map(c => plain(c.value)).join(' / ') || plain(d.note)}`;
|
|
93
|
+
return `- **${name(e.from)} → ${name(e.to)}** (role endpoints; ${state})\n Target: ${target || 'unknown'}. Purpose: ${detail(e.purpose)}. Integration: ${detail(e.integration)}.\n`;
|
|
94
|
+
}).join('') + `\n## Evidence and decisions\n\nSee research.html for exact sources, findings, competing claims and human decisions; surface.json retains property-level evidence and configuration references. Configured does not establish runtime use.\n\n## Gaps\n\n${s.gaps.map(g => '- ' + plain(g)).join('\n') || 'No additional gaps recorded; coverage remains bounded.'}\n\n## Recommended next steps\n\n${b.research.questions.map(q => '- ' + plain(q.text)).join('\n') || 'Review the evidence and confirm the next question before expanding coverage.'}\n`;
|
|
95
|
+
}
|
|
96
|
+
export function readBundle(directory) {
|
|
97
|
+
return guarded('saved-byte verification', () => {
|
|
98
|
+
directory = resolve(directory);
|
|
99
|
+
directories(directory);
|
|
100
|
+
const m = JSON.parse(read(join(directory, 'manifest.json')));
|
|
101
|
+
if (![1, 2].includes(m.version) || Object.keys(m.files ?? {}).sort().join(',') !== [...members].sort().join(','))
|
|
102
|
+
fail('manifest');
|
|
103
|
+
if (fs.readdirSync(directory).sort().join(',') !== [...members, 'manifest.json'].sort().join(','))
|
|
104
|
+
fail('unexpected members');
|
|
105
|
+
const data = Object.fromEntries(members.map(n => { const text = read(join(directory, n)); if (hash(text) !== m.files[n])
|
|
106
|
+
fail('changed bytes'); return [n, text]; }));
|
|
107
|
+
const b = checkBundle({ research: readResearchHtml(data['research.html']), surface: decode(data['surface.json'], 'json'), dependencies: decodeJsonValue(data['dependencies.json']) });
|
|
108
|
+
if (data['brief.md'] !== (m.version === 1 ? brief(b) : renderBrief(b)))
|
|
109
|
+
fail('brief conflict');
|
|
110
|
+
if (data['research.html'] !== (m.version === 1 ? renderResearchHtml(b.research) : renderBundleHtml(b)))
|
|
111
|
+
fail('presentation context conflict');
|
|
112
|
+
return b;
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export function verifyBundleSuccessor(parent, b) {
|
|
116
|
+
for (const old of [parent.research, parent.surface, ...parent.dependencies]) {
|
|
117
|
+
const next = [b.research, b.surface, ...b.dependencies].find(a => a.id === old.id && a.revision.id === old.revision.id);
|
|
118
|
+
if (next && digest(next) !== digest(old))
|
|
119
|
+
fail('rewritten pinned revision');
|
|
120
|
+
}
|
|
121
|
+
verifySuccessor(parent.research, b.research);
|
|
122
|
+
verifySuccessor(parent.surface, b.surface);
|
|
123
|
+
for (const d of parent.research.decisions)
|
|
124
|
+
if (!b.research.decisions.some(n => canonical(n) === canonical(d)))
|
|
125
|
+
fail('lost human decision');
|
|
126
|
+
for (const n of parent.surface.nodes)
|
|
127
|
+
if (!b.surface.nodes.some(x => x.id === n.id))
|
|
128
|
+
fail('lost component identity');
|
|
129
|
+
}
|
|
130
|
+
export function saveBundle(input, destination, predecessor) {
|
|
131
|
+
return guarded('save validation, predecessor or destination check', () => {
|
|
132
|
+
const b = checkBundle(input);
|
|
133
|
+
destination = resolve(destination);
|
|
134
|
+
directories(dirname(destination));
|
|
135
|
+
const parent = predecessor ? readBundle(predecessor) : undefined;
|
|
136
|
+
const previousBytes = predecessor ? read(join(predecessor, 'manifest.json')) : undefined;
|
|
137
|
+
if (parent) {
|
|
138
|
+
verifyBundleSuccessor(parent, b);
|
|
139
|
+
}
|
|
140
|
+
else if (b.research.revision.parent || b.surface.revision.parent)
|
|
141
|
+
fail('predecessor required');
|
|
142
|
+
const data = { 'research.html': renderBundleHtml(b), 'surface.json': encode(b.surface, 'json'), 'dependencies.json': JSON.stringify(b.dependencies, null, 2) + '\n', 'brief.md': renderBrief(b) };
|
|
143
|
+
const manifest = JSON.stringify({ version: 2, files: Object.fromEntries(members.map(n => [n, hash(data[n])])), predecessor: previousBytes ? hash(previousBytes) : null }, null, 2) + '\n';
|
|
144
|
+
// Parent must exist; exclusive mkdir never replaces an occupied destination.
|
|
145
|
+
fs.mkdirSync(destination, { mode: 0o700 });
|
|
146
|
+
try {
|
|
147
|
+
for (const [name, bytes] of Object.entries({ ...data, 'manifest.json': manifest }))
|
|
148
|
+
fs.writeFileSync(join(destination, name), bytes, { flag: 'wx', mode: 0o600 });
|
|
149
|
+
const result = readBundle(destination);
|
|
150
|
+
if (predecessor) {
|
|
151
|
+
readBundle(predecessor);
|
|
152
|
+
if (read(join(predecessor, 'manifest.json')) !== previousBytes)
|
|
153
|
+
fail('predecessor changed');
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
// Retain incomplete output for inspection; never call it a successful save.
|
|
159
|
+
throw e;
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
export function main(args) {
|
|
164
|
+
const [op, file, destination, previous] = args;
|
|
165
|
+
if (op === 'help' && args.length === 1) {
|
|
166
|
+
console.log('Standalone Surface helper: check BUNDLE.json | save BUNDLE.json NEW_DIRECTORY [PREDECESSOR_DIRECTORY] | read DIRECTORY. JSON bundle: {research,surface,dependencies}. Save only with user authorization. Node 22/24/26; no network or n8r-surface CLI.');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (op === 'read' && args.length === 2) {
|
|
170
|
+
const b = readBundle(file);
|
|
171
|
+
console.log(JSON.stringify({ status: 'checked', kind: b.surface.kind, research: b.research, surface: b.surface, dependencies: b.dependencies }, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if ((op === 'check' && args.length === 2) || (op === 'save' && (args.length === 3 || args.length === 4))) {
|
|
175
|
+
const b = checkBundle(decodeJsonValue(read(file)));
|
|
176
|
+
if (op === 'save')
|
|
177
|
+
saveBundle(b, destination, previous);
|
|
178
|
+
console.log(JSON.stringify({ status: op === 'save' ? 'saved-and-read-back' : 'checked', kind: b.surface.kind, researchDigest: digest(b.research), surfaceDigest: digest(b.surface) }));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
fail('arguments; run help');
|
|
182
|
+
}
|
|
183
|
+
/** Same canonical digest, with value-free diagnostics at the skill boundary. */
|
|
184
|
+
export function digest(value) { return guarded('digest input', () => artifactDigest(value)); }
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Bundle } from './skill-artifacts.js';
|
|
2
|
+
export interface MappingNode {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: 'component' | 'service';
|
|
5
|
+
type?: string;
|
|
6
|
+
rationale: string;
|
|
7
|
+
}
|
|
8
|
+
export interface Mapping {
|
|
9
|
+
nodes: MappingNode[];
|
|
10
|
+
dependencies: string[];
|
|
11
|
+
}
|
|
12
|
+
type Bom = Record<string, any>;
|
|
13
|
+
export interface Exchange {
|
|
14
|
+
bundle: Bundle;
|
|
15
|
+
mapping: Mapping;
|
|
16
|
+
bom: Bom;
|
|
17
|
+
report: Record<string, any>;
|
|
18
|
+
predecessor: string | null;
|
|
19
|
+
}
|
|
20
|
+
export { validateBom } from './cyclonedx-validation.js';
|
|
21
|
+
export declare function projectBundle(input: unknown, mapping: unknown, previous?: Exchange): {
|
|
22
|
+
bom: Bom;
|
|
23
|
+
report: Record<string, any>;
|
|
24
|
+
};
|
|
25
|
+
export declare function readExchange(directory: string): Exchange;
|
|
26
|
+
export declare function saveExchange(bundleDirectory: string, mapping: unknown, destination: string, previousDirectory?: string): Exchange;
|