release-skill 0.9.0 → 0.9.3
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +81 -0
- package/INSTALL.md +22 -9
- package/INSTALL.zh-CN.md +15 -8
- package/README.md +40 -35
- package/README.zh-CN.md +31 -29
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +110 -0
- package/adapters/claude/bin/error-codes.json +8 -1
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/registry.json +22 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/claude/schemas/release-project.schema.json +4 -0
- package/adapters/claude/schemas/release-run.schema.json +41 -1
- package/adapters/claude/skills/release-config/SKILL.md +5 -3
- package/adapters/claude/skills/release-docs/SKILL.md +4 -2
- package/adapters/claude/skills/release-finish/SKILL.md +6 -4
- package/adapters/claude/skills/release-help/SKILL.md +14 -5
- package/adapters/claude/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +4 -2
- package/adapters/claude/skills/release-publish/SKILL.md +19 -10
- package/adapters/claude/skills/release-verify/SKILL.md +2 -0
- package/adapters/claude/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/claude/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/claude/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/claude/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/claude/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/claude/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/claude/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/claude/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +110 -0
- package/adapters/codex/bin/error-codes.json +8 -1
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/registry.json +22 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/codex/schemas/release-project.schema.json +4 -0
- package/adapters/codex/schemas/release-run.schema.json +41 -1
- package/adapters/codex/skills/release-config/SKILL.md +5 -3
- package/adapters/codex/skills/release-docs/SKILL.md +4 -2
- package/adapters/codex/skills/release-finish/SKILL.md +6 -4
- package/adapters/codex/skills/release-help/SKILL.md +14 -5
- package/adapters/codex/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +4 -2
- package/adapters/codex/skills/release-publish/SKILL.md +19 -10
- package/adapters/codex/skills/release-verify/SKILL.md +2 -0
- package/adapters/codex/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/codex/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/codex/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/codex/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/codex/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/codex/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/codex/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/codex/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +110 -0
- package/adapters/kimi/bin/error-codes.json +8 -1
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/registry.json +22 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/kimi/schemas/release-project.schema.json +4 -0
- package/adapters/kimi/schemas/release-run.schema.json +41 -1
- package/adapters/kimi/skills/release-config/SKILL.md +5 -3
- package/adapters/kimi/skills/release-docs/SKILL.md +4 -2
- package/adapters/kimi/skills/release-finish/SKILL.md +6 -4
- package/adapters/kimi/skills/release-help/SKILL.md +14 -5
- package/adapters/kimi/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +4 -2
- package/adapters/kimi/skills/release-publish/SKILL.md +19 -10
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
- package/adapters/kimi/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/kimi/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/kimi/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/kimi/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/kimi/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/kimi/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/kimi/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +110 -0
- package/adapters/workbuddy/bin/error-codes.json +8 -1
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/registry.json +22 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +4 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +41 -1
- package/adapters/workbuddy/skills/release-config/SKILL.md +5 -3
- package/adapters/workbuddy/skills/release-docs/SKILL.md +4 -2
- package/adapters/workbuddy/skills/release-finish/SKILL.md +6 -4
- package/adapters/workbuddy/skills/release-help/SKILL.md +14 -5
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +4 -2
- package/adapters/workbuddy/skills/release-publish/SKILL.md +19 -10
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
- package/adapters/workbuddy/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/workbuddy/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/workbuddy/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/workbuddy/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/workbuddy/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/bin/consumer-contract-vectors.json +110 -0
- package/bin/error-codes.json +8 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/registry.json +22 -1
- package/bin/release-skill-cli.mjs +72 -4
- package/bin/release-skill.bundle.mjs +4772 -2238
- package/bin/rules.json +1 -1
- package/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/package.json +5 -5
- package/platform-manifest.json +5 -5
- package/references/01-state-machine.md +16 -1
- package/references/05-evidence-and-errors.md +2 -2
- package/schemas/release-project.schema.json +4 -0
- package/schemas/release-run.schema.json +41 -1
- package/skills/release-config/SKILL.md +5 -3
- package/skills/release-docs/SKILL.md +4 -2
- package/skills/release-finish/SKILL.md +6 -4
- package/skills/release-help/SKILL.md +14 -5
- package/skills/release-marketplace/SKILL.md +3 -1
- package/skills/release-prepare/SKILL.md +4 -2
- package/skills/release-publish/SKILL.md +19 -10
- package/skills/release-verify/SKILL.md +2 -0
- package/skills-src/release-config/SKILL.md +5 -3
- package/skills-src/release-docs/SKILL.md +4 -2
- package/skills-src/release-finish/SKILL.md +6 -4
- package/skills-src/release-help/SKILL.md +14 -5
- package/skills-src/release-marketplace/SKILL.md +3 -1
- package/skills-src/release-prepare/SKILL.md +4 -2
- package/skills-src/release-publish/SKILL.md +19 -10
- package/skills-src/release-verify/SKILL.md +2 -0
- package/src/commands/post-release-local.mjs +249 -58
- package/src/commands/prepare.mjs +178 -9
- package/src/commands/route.mjs +597 -18
- package/src/commands/setup.mjs +2 -0
- package/src/commands/ship.mjs +143 -16
- package/src/commands/verify.mjs +21 -0
- package/src/core/adoption-assessment.mjs +59 -0
- package/src/core/derived-artifact-gates.mjs +32 -5
- package/src/core/foundation-plugin-verification.mjs +213 -1
- package/src/core/hook-cache.mjs +318 -26
- package/src/core/hooks.mjs +8 -1
- package/src/producers/foundation-resource-projection.mjs +16 -3
- package/src/schemas/executable-identity-observation.schema.json +162 -0
- package/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/src/schemas/migration-manifest.schema.json +271 -7
- package/src/schemas/plugin-verification-request.schema.json +147 -22
- package/src/schemas/plugin-verification-result.schema.json +436 -18
- package/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
package/src/core/hook-cache.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Incremental hook result cache (T3.2).
|
|
3
3
|
*
|
|
4
4
|
* A hook that opts in with `cacheable: true` and a `cacheInputs` glob list is
|
|
5
|
-
* keyed by
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* keyed by a closed v2 receipt: its configuration and input closure, the
|
|
6
|
+
* allowlisted environment values, runtime and physical identities, plus the
|
|
7
|
+
* Foundation executable observation. When the key is unchanged and the last
|
|
8
|
+
* run succeeded, prepare replays the cached outcome instead of re-executing.
|
|
8
9
|
*
|
|
9
10
|
* Safety contract (see t3-2-incremental-hooks.md §4.8):
|
|
10
11
|
* - Failures are never cached. Only an `exitCode === 0` result is written; a
|
|
@@ -25,10 +26,22 @@
|
|
|
25
26
|
* @module hook-cache
|
|
26
27
|
*/
|
|
27
28
|
|
|
28
|
-
import { readdir,
|
|
29
|
-
import {
|
|
29
|
+
import { readdir, realpath, stat } from 'node:fs/promises';
|
|
30
|
+
import { release as osRelease } from 'node:os';
|
|
31
|
+
import { dirname, isAbsolute, join, relative, resolve } from 'node:path';
|
|
32
|
+
import {
|
|
33
|
+
createFilesystemRootBinding,
|
|
34
|
+
observeExecutableIdentity,
|
|
35
|
+
readFileStrict,
|
|
36
|
+
readFileBound,
|
|
37
|
+
publishFileExclusive,
|
|
38
|
+
publishFileOrReplace,
|
|
39
|
+
HARNESS_ERROR_KINDS,
|
|
40
|
+
} from 'skill-family-harness-node';
|
|
30
41
|
import { canonicalJson, sha256Hex } from './digest.mjs';
|
|
31
42
|
import { ReleaseError, GATE_FAILED } from './errors.mjs';
|
|
43
|
+
import { buildHookEnvironment } from './hooks.mjs';
|
|
44
|
+
import { readTrustedPackageResourceSync } from './trusted-resource.mjs';
|
|
32
45
|
|
|
33
46
|
/** Control-plane location of hook cache records: `.release-skill/cache/hooks`. */
|
|
34
47
|
const CACHE_BASE = ['.release-skill', 'cache', 'hooks'];
|
|
@@ -36,6 +49,20 @@ const CACHE_BASE = ['.release-skill', 'cache', 'hooks'];
|
|
|
36
49
|
/** Bounded tail length stored per stream (matches prepare's evidence tails). */
|
|
37
50
|
const TAIL_LENGTH = 4000;
|
|
38
51
|
|
|
52
|
+
const CACHE_SCHEMA_VERSION = 2;
|
|
53
|
+
const CACHE_KIND = 'release-skill.hook-cache';
|
|
54
|
+
const CACHE_ALGORITHM = 'release-skill-hook-cache-v2';
|
|
55
|
+
const CACHE_RECEIPT_VERSION = 1;
|
|
56
|
+
const CACHE_RECORD_KEYS = Object.freeze([
|
|
57
|
+
'algorithm', 'cacheKey', 'createdAt', 'exitCode', 'kind', 'receipt',
|
|
58
|
+
'receiptVersion', 'schemaVersion', 'stderrTail', 'stdoutTail',
|
|
59
|
+
]);
|
|
60
|
+
const CACHE_RECEIPT_KEYS = Object.freeze([
|
|
61
|
+
'algorithm', 'envDigest', 'executableObservationDigest', 'hookDigest',
|
|
62
|
+
'hookCwdIdentity', 'inputsDigest', 'projectRootIdentity', 'receiptVersion',
|
|
63
|
+
'releaseSkillVersion', 'runtime', 'schemaVersion',
|
|
64
|
+
]);
|
|
65
|
+
|
|
39
66
|
/**
|
|
40
67
|
* Directory names never walked when enumerating hook inputs. `.git` and
|
|
41
68
|
* `node_modules` are VCS/dependency internals; `.release-skill` is the control
|
|
@@ -88,30 +115,42 @@ function globToRegExp(glob) {
|
|
|
88
115
|
* @param {string} root - Absolute project root.
|
|
89
116
|
* @returns {Promise<string[]>} Sorted relative paths.
|
|
90
117
|
*/
|
|
91
|
-
async function listInputFiles(root) {
|
|
118
|
+
async function listInputFiles(root, matchers) {
|
|
92
119
|
const out = [];
|
|
120
|
+
let unsafe = false;
|
|
121
|
+
|
|
122
|
+
function matches(relPath) {
|
|
123
|
+
return matchers.some(({ re }) => re.test(relPath));
|
|
124
|
+
}
|
|
93
125
|
|
|
94
126
|
async function walk(dirAbs, dirRel) {
|
|
95
127
|
let entries;
|
|
96
128
|
try {
|
|
97
129
|
entries = await readdir(dirAbs, { withFileTypes: true });
|
|
98
130
|
} catch {
|
|
99
|
-
|
|
131
|
+
unsafe = true;
|
|
132
|
+
return;
|
|
100
133
|
}
|
|
101
134
|
for (const entry of entries) {
|
|
135
|
+
const rel = dirRel ? `${dirRel}/${entry.name}` : entry.name;
|
|
136
|
+
if (SKIPPED_DIRS.has(entry.name)) continue;
|
|
137
|
+
if (entry.isSymbolicLink()) {
|
|
138
|
+
if (matches(rel) || symlinkCouldCarryInput(rel, matchers)) unsafe = true;
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
102
141
|
if (entry.isDirectory()) {
|
|
103
|
-
if (SKIPPED_DIRS.has(entry.name)) continue;
|
|
104
|
-
const rel = dirRel ? `${dirRel}/${entry.name}` : entry.name;
|
|
105
142
|
await walk(join(dirAbs, entry.name), rel);
|
|
106
143
|
} else if (entry.isFile()) {
|
|
107
|
-
out.push(
|
|
144
|
+
out.push(rel);
|
|
145
|
+
} else if (matches(rel)) {
|
|
146
|
+
unsafe = true;
|
|
108
147
|
}
|
|
109
148
|
}
|
|
110
149
|
}
|
|
111
150
|
|
|
112
151
|
await walk(root, '');
|
|
113
152
|
out.sort();
|
|
114
|
-
return out;
|
|
153
|
+
return { files: out, unsafe };
|
|
115
154
|
}
|
|
116
155
|
|
|
117
156
|
/**
|
|
@@ -129,11 +168,186 @@ async function listInputFiles(root) {
|
|
|
129
168
|
* @returns {Promise<{ cacheKey: string, matchedFiles: string[] }>}
|
|
130
169
|
* @throws {ReleaseError} GATE_FAILED when any declared glob matches no file.
|
|
131
170
|
*/
|
|
132
|
-
|
|
171
|
+
function hasPathSeparator(command) {
|
|
172
|
+
return typeof command === 'string' && (command.includes('/') || command.includes('\\'));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function staticGlobBase(glob) {
|
|
176
|
+
const wildcard = glob.search(/[?*]/);
|
|
177
|
+
const prefix = wildcard === -1 ? glob : glob.slice(0, wildcard);
|
|
178
|
+
const slash = prefix.lastIndexOf('/');
|
|
179
|
+
return slash === -1 ? '' : prefix.slice(0, slash);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function symlinkCouldCarryInput(relPath, matchers) {
|
|
183
|
+
return matchers.some(({ glob }) => {
|
|
184
|
+
const base = staticGlobBase(glob);
|
|
185
|
+
return base === '' || relPath === base || relPath.startsWith(`${base}/`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function isWithin(root, candidate) {
|
|
190
|
+
const rel = relative(root, candidate);
|
|
191
|
+
return rel === '' || (rel !== '..' && !rel.startsWith('../') && !rel.startsWith('/'));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function nearestCommonDirectory(first, second) {
|
|
195
|
+
const firstParts = first.split('/').filter(Boolean);
|
|
196
|
+
const secondParts = second.split('/').filter(Boolean);
|
|
197
|
+
let length = 0;
|
|
198
|
+
while (length < firstParts.length && firstParts[length] === secondParts[length]) length += 1;
|
|
199
|
+
return length === 0 ? '/' : `/${firstParts.slice(0, length).join('/')}`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function executableBindingRoot(projectRoot, lexicalPath, targetPath, relativeCommand) {
|
|
203
|
+
if (relativeCommand) return projectRoot;
|
|
204
|
+
if (isWithin(projectRoot, lexicalPath) && isWithin(projectRoot, targetPath)) return projectRoot;
|
|
205
|
+
const common = nearestCommonDirectory(dirname(lexicalPath), dirname(targetPath));
|
|
206
|
+
if (common === '/') return null;
|
|
207
|
+
try {
|
|
208
|
+
const details = await stat(common);
|
|
209
|
+
if (!details.isDirectory()) return null;
|
|
210
|
+
const canonical = await realpath(common);
|
|
211
|
+
return canonical === '/' ? null : canonical;
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function physicalIdentity(path) {
|
|
218
|
+
const canonical = await realpath(path);
|
|
219
|
+
const details = await stat(canonical);
|
|
220
|
+
return {
|
|
221
|
+
path: canonical,
|
|
222
|
+
device: String(details.dev),
|
|
223
|
+
inode: String(details.ino),
|
|
224
|
+
mode: details.mode,
|
|
225
|
+
type: details.isDirectory() ? 'directory' : details.isFile() ? 'file' : 'other',
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function observeHookExecutable(hook, root, options = {}) {
|
|
230
|
+
if (process.platform === 'win32') return { eligible: false, reason: 'WINDOWS' };
|
|
231
|
+
const command = Array.isArray(hook.command) ? hook.command[0] : undefined;
|
|
232
|
+
if (!hasPathSeparator(command) || /^[A-Za-z]:[\\/]/.test(command)) {
|
|
233
|
+
return { eligible: false, reason: 'AMBIENT_PATH_OR_UNSUPPORTED_COMMAND' };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let projectRoot;
|
|
237
|
+
let cwd;
|
|
238
|
+
let executablePath;
|
|
239
|
+
try {
|
|
240
|
+
projectRoot = await realpath(root);
|
|
241
|
+
const declaredCwd = typeof hook.cwd === 'string' && hook.cwd.length > 0 ? hook.cwd : '.';
|
|
242
|
+
cwd = await realpath(resolve(projectRoot, declaredCwd));
|
|
243
|
+
if (!isWithin(projectRoot, cwd)) return { eligible: false, reason: 'CWD_OUTSIDE_PROJECT' };
|
|
244
|
+
executablePath = isAbsolute(command) ? resolve(command) : resolve(cwd, command);
|
|
245
|
+
if (!isAbsolute(command) && !isWithin(projectRoot, executablePath)) {
|
|
246
|
+
return { eligible: false, reason: 'EXECUTABLE_OUTSIDE_PROJECT' };
|
|
247
|
+
}
|
|
248
|
+
const targetPath = await realpath(executablePath);
|
|
249
|
+
const executableRoot = await executableBindingRoot(
|
|
250
|
+
projectRoot,
|
|
251
|
+
executablePath,
|
|
252
|
+
targetPath,
|
|
253
|
+
!isAbsolute(command),
|
|
254
|
+
);
|
|
255
|
+
if (!executableRoot) return { eligible: false, reason: 'BOUND_ROOT_TOO_BROAD' };
|
|
256
|
+
// The executable observation receives one minimal root covering the
|
|
257
|
+
// lexical absolute entry and its physical target. The project root binding
|
|
258
|
+
// is separate and is used only for cache-record IO.
|
|
259
|
+
const executableBinding = await createFilesystemRootBinding(executableRoot);
|
|
260
|
+
const projectRootBinding = executableRoot === projectRoot
|
|
261
|
+
? executableBinding : await createFilesystemRootBinding(projectRoot);
|
|
262
|
+
try {
|
|
263
|
+
const observe = options.observeExecutableIdentityFn ?? observeExecutableIdentity;
|
|
264
|
+
const observed = await observe({
|
|
265
|
+
boundRoots: [{ root: executableRoot, rootBinding: executableBinding }],
|
|
266
|
+
lookup: { mode: 'absolute-path', path: executablePath },
|
|
267
|
+
});
|
|
268
|
+
if (!observed || !isDigest(observed.observationDigest)) {
|
|
269
|
+
return { eligible: false, reason: 'OBSERVATION_INVALID' };
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
eligible: true,
|
|
273
|
+
observationDigest: observed.observationDigest,
|
|
274
|
+
projectRoot: await physicalIdentity(projectRoot),
|
|
275
|
+
hookCwd: await physicalIdentity(cwd),
|
|
276
|
+
projectRootBinding,
|
|
277
|
+
};
|
|
278
|
+
} catch {
|
|
279
|
+
return { eligible: false, reason: 'OBSERVATION_FAILED' };
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
return { eligible: false, reason: 'OBSERVATION_FAILED' };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function sortedObject(object) {
|
|
287
|
+
return Object.fromEntries(Object.keys(object).sort().map((key) => [key, object[key]]));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function hasExactKeys(value, keys) {
|
|
291
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
292
|
+
&& Object.keys(value).sort().join('\0') === keys.slice().sort().join('\0');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isDigest(value) {
|
|
296
|
+
return typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isValidIdentity(value) {
|
|
300
|
+
return hasExactKeys(value, ['device', 'inode', 'mode', 'path', 'type'])
|
|
301
|
+
&& typeof value.path === 'string'
|
|
302
|
+
&& typeof value.device === 'string'
|
|
303
|
+
&& typeof value.inode === 'string'
|
|
304
|
+
&& Number.isInteger(value.mode)
|
|
305
|
+
&& ['directory', 'file', 'other'].includes(value.type);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function readCurrentReleaseSkillVersion() {
|
|
309
|
+
const validVersion = (version) => typeof version === 'string'
|
|
310
|
+
&& version.length > 0 && version.length <= 128 && !/[\u0000-\u001f\u007f]/.test(version)
|
|
311
|
+
? version : null;
|
|
312
|
+
if (typeof __bundlePkg !== 'undefined' && __bundlePkg && typeof __bundlePkg.version === 'string') {
|
|
313
|
+
return validVersion(__bundlePkg.version);
|
|
314
|
+
}
|
|
315
|
+
try {
|
|
316
|
+
const pkg = JSON.parse(readTrustedPackageResourceSync('package.json').toString('utf8'));
|
|
317
|
+
return validVersion(pkg.version);
|
|
318
|
+
} catch {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function isValidReceipt(receipt, cacheKey, currentVersion) {
|
|
324
|
+
if (!hasExactKeys(receipt, CACHE_RECEIPT_KEYS)) return false;
|
|
325
|
+
if (receipt.schemaVersion !== CACHE_SCHEMA_VERSION
|
|
326
|
+
|| receipt.algorithm !== CACHE_ALGORITHM
|
|
327
|
+
|| receipt.receiptVersion !== CACHE_RECEIPT_VERSION
|
|
328
|
+
|| receipt.releaseSkillVersion !== currentVersion
|
|
329
|
+
|| !isDigest(receipt.hookDigest)
|
|
330
|
+
|| !isDigest(receipt.inputsDigest)
|
|
331
|
+
|| !isDigest(receipt.envDigest)
|
|
332
|
+
|| !isDigest(receipt.executableObservationDigest)
|
|
333
|
+
|| !isValidIdentity(receipt.projectRootIdentity)
|
|
334
|
+
|| !isValidIdentity(receipt.hookCwdIdentity)
|
|
335
|
+
|| !hasExactKeys(receipt.runtime, ['arch', 'node', 'os', 'platform'])
|
|
336
|
+
|| typeof receipt.runtime.arch !== 'string'
|
|
337
|
+
|| typeof receipt.runtime.node !== 'string'
|
|
338
|
+
|| typeof receipt.runtime.os !== 'string'
|
|
339
|
+
|| typeof receipt.runtime.platform !== 'string') return false;
|
|
340
|
+
return sha256Hex(canonicalJson(receipt)) === cacheKey;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export async function computeHookCacheKey(hook, root, options = {}) {
|
|
133
344
|
const globs = Array.isArray(hook.cacheInputs) ? hook.cacheInputs : [];
|
|
134
345
|
const matchers = globs.map((glob) => ({ glob, re: globToRegExp(glob) }));
|
|
135
346
|
|
|
136
|
-
const allFiles = await listInputFiles(root);
|
|
347
|
+
const { files: allFiles, unsafe: unsafeInputs } = await listInputFiles(root, matchers);
|
|
348
|
+
if (unsafeInputs) {
|
|
349
|
+
return { cacheKey: null, matchedFiles: [], cacheable: false, reason: 'INPUT_CLOSURE_UNSAFE' };
|
|
350
|
+
}
|
|
137
351
|
const matched = [];
|
|
138
352
|
const hitPerGlob = matchers.map(() => false);
|
|
139
353
|
for (const relPath of allFiles) {
|
|
@@ -162,15 +376,47 @@ export async function computeHookCacheKey(hook, root) {
|
|
|
162
376
|
matched.sort();
|
|
163
377
|
const fileEntries = [];
|
|
164
378
|
for (const relPath of matched) {
|
|
165
|
-
|
|
166
|
-
|
|
379
|
+
try {
|
|
380
|
+
const content = await readFileStrict(root, relPath);
|
|
381
|
+
if (!content || !Buffer.isBuffer(content.content) || !isDigest(content.sha256)) {
|
|
382
|
+
return { cacheKey: null, matchedFiles: matched, cacheable: false, reason: 'INPUT_READ_FAILED' };
|
|
383
|
+
}
|
|
384
|
+
fileEntries.push({ path: relPath, sha256: content.sha256 });
|
|
385
|
+
} catch {
|
|
386
|
+
return { cacheKey: null, matchedFiles: matched, cacheable: false, reason: 'INPUT_READ_FAILED' };
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const observed = await observeHookExecutable(hook, root, options);
|
|
391
|
+
if (!observed.eligible) {
|
|
392
|
+
return { cacheKey: null, matchedFiles: matched, cacheable: false, reason: observed.reason };
|
|
167
393
|
}
|
|
168
394
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
395
|
+
const releaseSkillVersion = readCurrentReleaseSkillVersion();
|
|
396
|
+
if (releaseSkillVersion === null) {
|
|
397
|
+
return { cacheKey: null, matchedFiles: matched, cacheable: false, reason: 'VERSION_UNAVAILABLE' };
|
|
398
|
+
}
|
|
399
|
+
const selectedEnv = buildHookEnvironment(hook.envAllowlist ?? [], options.env ?? process.env);
|
|
400
|
+
const receipt = {
|
|
401
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
402
|
+
algorithm: CACHE_ALGORITHM,
|
|
403
|
+
receiptVersion: CACHE_RECEIPT_VERSION,
|
|
404
|
+
hookDigest: sha256Hex(canonicalJson(hook)),
|
|
405
|
+
inputsDigest: sha256Hex(canonicalJson(fileEntries)),
|
|
406
|
+
envDigest: sha256Hex(canonicalJson(sortedObject(selectedEnv))),
|
|
407
|
+
releaseSkillVersion,
|
|
408
|
+
runtime: {
|
|
409
|
+
platform: process.platform,
|
|
410
|
+
arch: process.arch,
|
|
411
|
+
os: osRelease(),
|
|
412
|
+
node: process.versions.node,
|
|
413
|
+
},
|
|
414
|
+
projectRootIdentity: observed.projectRoot,
|
|
415
|
+
hookCwdIdentity: observed.hookCwd,
|
|
416
|
+
executableObservationDigest: observed.observationDigest,
|
|
417
|
+
};
|
|
418
|
+
const cacheKey = sha256Hex(canonicalJson(receipt));
|
|
419
|
+
return { cacheKey, matchedFiles: matched, cacheable: true, receipt };
|
|
174
420
|
}
|
|
175
421
|
|
|
176
422
|
/**
|
|
@@ -196,6 +442,10 @@ export function hookCachePath(root, hookName, cacheKey) {
|
|
|
196
442
|
return join(hookCacheDir(root, hookName), `${cacheKey}.json`);
|
|
197
443
|
}
|
|
198
444
|
|
|
445
|
+
function hookCacheRelativePath(hookName, cacheKey) {
|
|
446
|
+
return join(...CACHE_BASE, hookName, `${cacheKey}.json`);
|
|
447
|
+
}
|
|
448
|
+
|
|
199
449
|
/**
|
|
200
450
|
* Read a cached hook result. Returns the record only when it exists, its key
|
|
201
451
|
* matches, and it recorded a successful (`exitCode === 0`) run; anything else
|
|
@@ -206,11 +456,36 @@ export function hookCachePath(root, hookName, cacheKey) {
|
|
|
206
456
|
* @param {string} cacheKey
|
|
207
457
|
* @returns {Promise<Object | null>}
|
|
208
458
|
*/
|
|
209
|
-
|
|
459
|
+
async function resolveCacheRoot(root, suppliedBinding) {
|
|
460
|
+
const projectRoot = await realpath(root);
|
|
461
|
+
const rootBinding = suppliedBinding ?? await createFilesystemRootBinding(projectRoot);
|
|
462
|
+
return { projectRoot, rootBinding };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export async function readHookCache(root, hookName, cacheKey, options = {}) {
|
|
210
466
|
try {
|
|
211
|
-
const
|
|
467
|
+
const currentVersion = readCurrentReleaseSkillVersion();
|
|
468
|
+
if (currentVersion === null) return null;
|
|
469
|
+
const { projectRoot, rootBinding } = await resolveCacheRoot(root, options.rootBinding);
|
|
470
|
+
const rawReceipt = await readFileBound(projectRoot, hookCacheRelativePath(hookName, cacheKey), {
|
|
471
|
+
rootBinding,
|
|
472
|
+
encoding: 'utf8',
|
|
473
|
+
});
|
|
474
|
+
const raw = rawReceipt?.content;
|
|
475
|
+
if (typeof raw !== 'string') return null;
|
|
212
476
|
const record = JSON.parse(raw);
|
|
213
|
-
if (record && record.cacheKey === cacheKey && record.exitCode === 0
|
|
477
|
+
if (record && record.cacheKey === cacheKey && record.exitCode === 0
|
|
478
|
+
&& hasExactKeys(record, CACHE_RECORD_KEYS)
|
|
479
|
+
&& record.schemaVersion === CACHE_SCHEMA_VERSION
|
|
480
|
+
&& record.kind === CACHE_KIND
|
|
481
|
+
&& record.algorithm === CACHE_ALGORITHM
|
|
482
|
+
&& record.receiptVersion === CACHE_RECEIPT_VERSION
|
|
483
|
+
&& typeof record.createdAt === 'string'
|
|
484
|
+
&& typeof record.stdoutTail === 'string'
|
|
485
|
+
&& typeof record.stderrTail === 'string'
|
|
486
|
+
&& record.stdoutTail.length <= TAIL_LENGTH
|
|
487
|
+
&& record.stderrTail.length <= TAIL_LENGTH
|
|
488
|
+
&& isValidReceipt(record.receipt, cacheKey, currentVersion)) {
|
|
214
489
|
return record;
|
|
215
490
|
}
|
|
216
491
|
return null;
|
|
@@ -232,24 +507,41 @@ export async function readHookCache(root, hookName, cacheKey) {
|
|
|
232
507
|
* @param {string} [result.stdoutTail] - Already truncated to TAIL_LENGTH.
|
|
233
508
|
* @param {string} [result.stderrTail] - Already truncated to TAIL_LENGTH.
|
|
234
509
|
* @param {string} [result.createdAt] - ISO timestamp (defaults to now).
|
|
510
|
+
* @param {Object} result.receipt - The v2 cache receipt produced with the key.
|
|
235
511
|
* @returns {Promise<{ ok: true } | { ok: false, error: string }>}
|
|
236
512
|
*/
|
|
237
|
-
export async function writeHookCache(root, hookName, cacheKey, result) {
|
|
513
|
+
export async function writeHookCache(root, hookName, cacheKey, result, options = {}) {
|
|
238
514
|
// Defence in depth: a failure must never be persisted, even if a caller
|
|
239
515
|
// mistakenly passes a non-zero exit code.
|
|
240
516
|
if (!result || result.exitCode !== 0) {
|
|
241
517
|
return { ok: false, error: 'refusing to cache a non-zero exit result' };
|
|
242
518
|
}
|
|
243
519
|
try {
|
|
244
|
-
|
|
520
|
+
const currentVersion = readCurrentReleaseSkillVersion();
|
|
521
|
+
if (!isValidReceipt(result.receipt, cacheKey, currentVersion)) {
|
|
522
|
+
return { ok: false, error: 'refusing to cache without a v2 receipt' };
|
|
523
|
+
}
|
|
245
524
|
const record = {
|
|
525
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
526
|
+
kind: CACHE_KIND,
|
|
527
|
+
algorithm: CACHE_ALGORITHM,
|
|
528
|
+
receiptVersion: CACHE_RECEIPT_VERSION,
|
|
246
529
|
cacheKey,
|
|
247
530
|
exitCode: 0,
|
|
248
531
|
stdoutTail: String(result.stdoutTail ?? '').slice(-TAIL_LENGTH),
|
|
249
532
|
stderrTail: String(result.stderrTail ?? '').slice(-TAIL_LENGTH),
|
|
250
533
|
createdAt: result.createdAt ?? new Date().toISOString(),
|
|
534
|
+
receipt: result.receipt,
|
|
251
535
|
};
|
|
252
|
-
|
|
536
|
+
const { projectRoot, rootBinding } = await resolveCacheRoot(root, options.rootBinding);
|
|
537
|
+
const relPath = hookCacheRelativePath(hookName, cacheKey);
|
|
538
|
+
const bytes = `${JSON.stringify(record, null, 2)}\n`;
|
|
539
|
+
try {
|
|
540
|
+
await publishFileExclusive(projectRoot, relPath, bytes, { mode: 0o644, createParents: true });
|
|
541
|
+
} catch (err) {
|
|
542
|
+
if (err?.details?.kind !== HARNESS_ERROR_KINDS.EXCLUSIVE_PUBLISH_CONFLICT) throw err;
|
|
543
|
+
await publishFileOrReplace(projectRoot, relPath, bytes, { mode: 0o644 });
|
|
544
|
+
}
|
|
253
545
|
return { ok: true };
|
|
254
546
|
} catch (err) {
|
|
255
547
|
return { ok: false, error: err.message };
|
package/src/core/hooks.mjs
CHANGED
|
@@ -161,7 +161,12 @@ export function validateHook(hook) {
|
|
|
161
161
|
* @param {Record<string, string>} [contextEnv] - Caller-provided env map.
|
|
162
162
|
* @returns {Record<string, string>}
|
|
163
163
|
*/
|
|
164
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Resolve the exact environment map delivered to a declared Hook. This is an
|
|
166
|
+
* internal shared seam for cache-key construction; it deliberately keeps the
|
|
167
|
+
* existing platform-required and allowlist precedence rules in one place.
|
|
168
|
+
*/
|
|
169
|
+
export function buildHookEnvironment(envAllowlist = [], contextEnv) {
|
|
165
170
|
const env = {};
|
|
166
171
|
|
|
167
172
|
// 1. Platform-required variables
|
|
@@ -184,6 +189,8 @@ function buildFilteredEnv(envAllowlist, contextEnv) {
|
|
|
184
189
|
return env;
|
|
185
190
|
}
|
|
186
191
|
|
|
192
|
+
const buildFilteredEnv = buildHookEnvironment;
|
|
193
|
+
|
|
187
194
|
// ---------------------------------------------------------------------------
|
|
188
195
|
// Public API
|
|
189
196
|
// ---------------------------------------------------------------------------
|
|
@@ -55,9 +55,9 @@ import { digestBytes, publishFileOrReplace, readFileStrict, withTemporaryWorkspa
|
|
|
55
55
|
import { canonicalJson } from '../core/digest.mjs';
|
|
56
56
|
|
|
57
57
|
export const FOUNDATION_PACKAGES = Object.freeze({
|
|
58
|
-
'skill-family-contracts': '0.
|
|
59
|
-
'skill-family-engineering-kit': '0.
|
|
60
|
-
'skill-family-harness-node': '0.
|
|
58
|
+
'skill-family-contracts': '0.15.0',
|
|
59
|
+
'skill-family-engineering-kit': '0.15.0',
|
|
60
|
+
'skill-family-harness-node': '0.15.0',
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
export const BINDING_RECORD_PATH = 'bin/foundation-resource-binding.json';
|
|
@@ -295,6 +295,19 @@ export async function computeFoundationProjection({ pkgRoot, bundleContent }) {
|
|
|
295
295
|
continue;
|
|
296
296
|
}
|
|
297
297
|
}
|
|
298
|
+
// Foundation 0.14 keeps this example behind a capability-selection
|
|
299
|
+
// branch that esbuild removes from the release-skill bundle. The
|
|
300
|
+
// source scanner still sees the directory URL in the marked module,
|
|
301
|
+
// so admit this one exact directory only when its path survives in
|
|
302
|
+
// the built bundle.
|
|
303
|
+
if (
|
|
304
|
+
pkg === 'skill-family-engineering-kit'
|
|
305
|
+
&& toPosix(relative(packages[pkg].root, sourceDir)) === 'data/examples/minimal-plugin-payload'
|
|
306
|
+
&& !bundleContent.includes('data/examples/minimal-plugin-payload')
|
|
307
|
+
) {
|
|
308
|
+
scopeOf(targetPosix);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
298
311
|
const files = await walkTree(sourceDir, `${pkg} directory read "${rel}"`);
|
|
299
312
|
const members = [];
|
|
300
313
|
for (const file of files) {
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://contracts.skill-family.example/v1/executable-identity-observation.json",
|
|
4
|
+
"title": "Executable identity observation",
|
|
5
|
+
"description": "A single, private observation of an executable lookup and its explicit launch image. Absolute roots and file bytes are intentionally represented only by the bound root and digest facts.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"required": ["schemaVersion", "kind", "lookup", "canonicalEntry", "interpreter", "launch", "observationDigest"],
|
|
9
|
+
"properties": {
|
|
10
|
+
"schemaVersion": { "const": 1 },
|
|
11
|
+
"kind": { "const": "skill-family.executable-identity-observation" },
|
|
12
|
+
"lookup": { "$ref": "#/$defs/lookup" },
|
|
13
|
+
"canonicalEntry": { "$ref": "#/$defs/canonicalEntry" },
|
|
14
|
+
"interpreter": {
|
|
15
|
+
"oneOf": [
|
|
16
|
+
{ "type": "null" },
|
|
17
|
+
{
|
|
18
|
+
"type": "object",
|
|
19
|
+
"additionalProperties": false,
|
|
20
|
+
"required": ["request", "canonicalEntry", "shebangArgs"],
|
|
21
|
+
"properties": {
|
|
22
|
+
"request": {
|
|
23
|
+
"oneOf": [
|
|
24
|
+
{ "$ref": "#/$defs/bareCommand" },
|
|
25
|
+
{ "$ref": "#/$defs/absolutePath" }
|
|
26
|
+
]
|
|
27
|
+
},
|
|
28
|
+
"canonicalEntry": {
|
|
29
|
+
"allOf": [
|
|
30
|
+
{ "$ref": "#/$defs/canonicalEntry" },
|
|
31
|
+
{ "type": "object", "properties": { "entryKind": { "const": "native-binary" } } }
|
|
32
|
+
]
|
|
33
|
+
},
|
|
34
|
+
"shebangArgs": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"maxItems": 8,
|
|
37
|
+
"items": { "type": "string", "minLength": 1, "maxLength": 256, "pattern": "^[^\\u0000\\r\\n]+$" }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"launch": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"additionalProperties": false,
|
|
46
|
+
"required": ["file", "argvPrefix"],
|
|
47
|
+
"properties": {
|
|
48
|
+
"file": { "$ref": "#/$defs/absolutePath" },
|
|
49
|
+
"argvPrefix": {
|
|
50
|
+
"type": "array",
|
|
51
|
+
"maxItems": 8,
|
|
52
|
+
"items": { "$ref": "#/$defs/absolutePath" }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
"observationDigest": { "$ref": "#/$defs/sha256" }
|
|
57
|
+
},
|
|
58
|
+
"allOf": [
|
|
59
|
+
{
|
|
60
|
+
"if": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"properties": { "canonicalEntry": { "type": "object", "properties": { "entryKind": { "const": "interpreter-script" } }, "required": ["entryKind"] } },
|
|
63
|
+
"required": ["canonicalEntry"]
|
|
64
|
+
},
|
|
65
|
+
"then": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"interpreter": { "type": "object" },
|
|
69
|
+
"launch": { "type": "object", "properties": { "argvPrefix": { "type": "array", "minItems": 1 } } }
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"else": {
|
|
73
|
+
"type": "object",
|
|
74
|
+
"properties": {
|
|
75
|
+
"interpreter": { "type": "null" },
|
|
76
|
+
"launch": { "type": "object", "properties": { "argvPrefix": { "type": "array", "maxItems": 0 } } }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
],
|
|
81
|
+
"$defs": {
|
|
82
|
+
"sha256": { "type": "string", "pattern": "^[0-9a-f]{64}$" },
|
|
83
|
+
"absolutePath": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"minLength": 1,
|
|
86
|
+
"maxLength": 4096,
|
|
87
|
+
"pattern": "^/(?:[^/\\\\\\u0000]+(?:/[^/\\\\\\u0000]+)*)?$",
|
|
88
|
+
"not": { "pattern": "(?:^|/)\\.{1,2}(?:/|$)" }
|
|
89
|
+
},
|
|
90
|
+
"relativePath": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"minLength": 1,
|
|
93
|
+
"maxLength": 4096,
|
|
94
|
+
"pattern": "^(?!/)(?!.*(?:^|/)\\.\\.(?:/|$))(?!.*(?:^|/)\\.(?:/|$))(?!.*\\\\)(?!.*\\u0000)[^/\\\\\\u0000]+(?:/[^/\\\\\\u0000]+)*$"
|
|
95
|
+
},
|
|
96
|
+
"bareCommand": {
|
|
97
|
+
"type": "string",
|
|
98
|
+
"minLength": 1,
|
|
99
|
+
"maxLength": 256,
|
|
100
|
+
"pattern": "^[^/\\\\\\u0000]+$"
|
|
101
|
+
},
|
|
102
|
+
"rootBinding": { "$ref": "https://contracts.skill-family.example/v1/filesystem-root-binding.json" },
|
|
103
|
+
"lookup": {
|
|
104
|
+
"type": "object",
|
|
105
|
+
"additionalProperties": false,
|
|
106
|
+
"required": ["requested", "mode", "selectedPath"],
|
|
107
|
+
"properties": {
|
|
108
|
+
"requested": { "type": "string", "minLength": 1, "maxLength": 4096 },
|
|
109
|
+
"mode": { "enum": ["absolute-path", "explicit-path-search"] },
|
|
110
|
+
"path": { "$ref": "#/$defs/absolutePath" },
|
|
111
|
+
"pathEntries": {
|
|
112
|
+
"type": "array",
|
|
113
|
+
"minItems": 1,
|
|
114
|
+
"maxItems": 64,
|
|
115
|
+
"uniqueItems": true,
|
|
116
|
+
"items": { "$ref": "#/$defs/absolutePath" }
|
|
117
|
+
},
|
|
118
|
+
"selectedPath": { "$ref": "#/$defs/absolutePath" }
|
|
119
|
+
},
|
|
120
|
+
"allOf": [
|
|
121
|
+
{
|
|
122
|
+
"if": { "type": "object", "properties": { "mode": { "const": "absolute-path" } }, "required": ["mode"] },
|
|
123
|
+
"then": {
|
|
124
|
+
"type": "object",
|
|
125
|
+
"required": ["path"],
|
|
126
|
+
"properties": { "path": {}, "pathEntries": false }
|
|
127
|
+
},
|
|
128
|
+
"else": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"required": ["pathEntries"],
|
|
131
|
+
"properties": { "pathEntries": {}, "path": false }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
]
|
|
135
|
+
},
|
|
136
|
+
"symlink": {
|
|
137
|
+
"type": "object",
|
|
138
|
+
"additionalProperties": false,
|
|
139
|
+
"required": ["path", "targetBase64", "bytes"],
|
|
140
|
+
"properties": {
|
|
141
|
+
"path": { "$ref": "#/$defs/absolutePath" },
|
|
142
|
+
"targetBase64": { "type": "string", "pattern": "^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$" },
|
|
143
|
+
"bytes": { "type": "integer", "minimum": 0, "maximum": 1048576 }
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
"canonicalEntry": {
|
|
147
|
+
"type": "object",
|
|
148
|
+
"additionalProperties": false,
|
|
149
|
+
"required": ["path", "symlinkChain", "binding", "relPath", "sha256", "bytes", "statMode", "entryKind"],
|
|
150
|
+
"properties": {
|
|
151
|
+
"path": { "$ref": "#/$defs/absolutePath" },
|
|
152
|
+
"symlinkChain": { "type": "array", "maxItems": 40, "items": { "$ref": "#/$defs/symlink" } },
|
|
153
|
+
"binding": { "$ref": "#/$defs/rootBinding" },
|
|
154
|
+
"relPath": { "$ref": "#/$defs/relativePath" },
|
|
155
|
+
"sha256": { "$ref": "#/$defs/sha256" },
|
|
156
|
+
"bytes": { "type": "integer", "minimum": 0, "maximum": 1073741824 },
|
|
157
|
+
"statMode": { "type": "integer", "minimum": 0, "maximum": 4294967295 },
|
|
158
|
+
"entryKind": { "enum": ["native-binary", "interpreter-script", "opaque-executable"] }
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|