@jinn-network/jinn-layer 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/dist/adapters/contribution-adapter.d.ts +18 -0
- package/dist/adapters/contribution-adapter.js +134 -0
- package/dist/adapters/corpus-adapter.d.ts +12 -0
- package/dist/adapters/corpus-adapter.js +212 -0
- package/dist/adapters/index.d.ts +17 -0
- package/dist/adapters/index.js +17 -0
- package/dist/adapters/json-map-store.d.ts +4 -0
- package/dist/adapters/json-map-store.js +31 -0
- package/dist/adapters/local-learning-adapter.d.ts +25 -0
- package/dist/adapters/local-learning-adapter.js +49 -0
- package/dist/adapters/skills-adapter.d.ts +8 -0
- package/dist/adapters/skills-adapter.js +55 -0
- package/dist/admission-policy.d.ts +38 -0
- package/dist/admission-policy.js +17 -0
- package/dist/bin/jinn-distill-mcp.d.ts +2 -0
- package/dist/bin/jinn-distill-mcp.js +10 -0
- package/dist/bin/jinn-layer.d.ts +7 -0
- package/dist/bin/jinn-layer.js +15 -0
- package/dist/bridge-fetch-evidence.d.ts +102 -0
- package/dist/bridge-fetch-evidence.js +677 -0
- package/dist/bridge-verdict-source.d.ts +47 -0
- package/dist/bridge-verdict-source.js +216 -0
- package/dist/bridge.d.ts +247 -0
- package/dist/bridge.js +439 -0
- package/dist/capture.d.ts +98 -0
- package/dist/capture.js +617 -0
- package/dist/cli.d.ts +148 -0
- package/dist/cli.js +2221 -0
- package/dist/cluster-selection.d.ts +34 -0
- package/dist/cluster-selection.js +135 -0
- package/dist/cluster.d.ts +89 -0
- package/dist/cluster.js +142 -0
- package/dist/consume.d.ts +139 -0
- package/dist/consume.js +280 -0
- package/dist/corpus-probes.d.ts +61 -0
- package/dist/corpus-probes.js +120 -0
- package/dist/corpus-store.d.ts +16 -0
- package/dist/corpus-store.js +101 -0
- package/dist/distill-captures.d.ts +56 -0
- package/dist/distill-captures.js +283 -0
- package/dist/distill-feedback.d.ts +20 -0
- package/dist/distill-feedback.js +30 -0
- package/dist/distill-llm.d.ts +138 -0
- package/dist/distill-llm.js +438 -0
- package/dist/distill-mcp-server.d.ts +30 -0
- package/dist/distill-mcp-server.js +281 -0
- package/dist/distill-mode.d.ts +62 -0
- package/dist/distill-mode.js +107 -0
- package/dist/distill-progress.d.ts +78 -0
- package/dist/distill-progress.js +69 -0
- package/dist/distill-prompt.d.ts +34 -0
- package/dist/distill-prompt.js +85 -0
- package/dist/distill-render.d.ts +123 -0
- package/dist/distill-render.js +303 -0
- package/dist/distill-runs.d.ts +28 -0
- package/dist/distill-runs.js +39 -0
- package/dist/distill-traces.d.ts +67 -0
- package/dist/distill-traces.js +211 -0
- package/dist/distill.d.ts +180 -0
- package/dist/distill.js +337 -0
- package/dist/distiller.d.ts +118 -0
- package/dist/distiller.js +142 -0
- package/dist/envelope.d.ts +9 -0
- package/dist/envelope.js +9 -0
- package/dist/eval-prep.d.ts +129 -0
- package/dist/eval-prep.js +523 -0
- package/dist/execution-publish.d.ts +62 -0
- package/dist/execution-publish.js +72 -0
- package/dist/gate.d.ts +39 -0
- package/dist/gate.js +106 -0
- package/dist/index.d.ts +49 -0
- package/dist/index.js +50 -0
- package/dist/ipfs-cid.d.ts +18 -0
- package/dist/ipfs-cid.js +149 -0
- package/dist/layer-default.d.ts +3 -0
- package/dist/layer-default.js +10 -0
- package/dist/ledger.d.ts +62 -0
- package/dist/ledger.js +104 -0
- package/dist/measurement.d.ts +90 -0
- package/dist/measurement.js +134 -0
- package/dist/path-safety.d.ts +19 -0
- package/dist/path-safety.js +303 -0
- package/dist/pipeline.d.ts +94 -0
- package/dist/pipeline.js +124 -0
- package/dist/plugin-wiring.d.ts +4 -0
- package/dist/plugin-wiring.js +54 -0
- package/dist/preview.d.ts +31 -0
- package/dist/preview.js +28 -0
- package/dist/process-contract.d.ts +303 -0
- package/dist/process-contract.js +118 -0
- package/dist/publish-skill.d.ts +86 -0
- package/dist/publish-skill.js +116 -0
- package/dist/publish.d.ts +294 -0
- package/dist/publish.js +1090 -0
- package/dist/scripts/scan-skills-registry.d.ts +24 -0
- package/dist/scripts/scan-skills-registry.js +207 -0
- package/dist/seed-import/curated-batch.d.ts +36 -0
- package/dist/seed-import/curated-batch.js +103 -0
- package/dist/seed-import/episode-execute.d.ts +50 -0
- package/dist/seed-import/episode-execute.js +235 -0
- package/dist/seed-import/episode-fetch.d.ts +119 -0
- package/dist/seed-import/episode-fetch.js +137 -0
- package/dist/seed-import/episode-plan.d.ts +11 -0
- package/dist/seed-import/episode-plan.js +27 -0
- package/dist/seed-import/episode-report.d.ts +39 -0
- package/dist/seed-import/episode-report.js +36 -0
- package/dist/seed-import/execute.d.ts +61 -0
- package/dist/seed-import/execute.js +245 -0
- package/dist/seed-import/fetch.d.ts +62 -0
- package/dist/seed-import/fetch.js +82 -0
- package/dist/seed-import/licence.d.ts +17 -0
- package/dist/seed-import/licence.js +36 -0
- package/dist/seed-import/plan.d.ts +10 -0
- package/dist/seed-import/plan.js +21 -0
- package/dist/seed-import/report.d.ts +34 -0
- package/dist/seed-import/report.js +31 -0
- package/dist/seed-import/state.d.ts +72 -0
- package/dist/seed-import/state.js +120 -0
- package/dist/signal.d.ts +36 -0
- package/dist/signal.js +48 -0
- package/dist/signing.d.ts +8 -0
- package/dist/signing.js +21 -0
- package/dist/skill-package.d.ts +70 -0
- package/dist/skill-package.js +141 -0
- package/dist/skill.d.ts +21 -0
- package/dist/skill.js +77 -0
- package/dist/snapshot-transcript.d.ts +54 -0
- package/dist/snapshot-transcript.js +117 -0
- package/dist/three-arm-run.d.ts +27 -0
- package/dist/three-arm-run.js +41 -0
- package/package.json +60 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `execute()` — publish the approved import rows through the Task-4 path
|
|
3
|
+
* (plan Task 6, spec §7).
|
|
4
|
+
*
|
|
5
|
+
* Each imported skill becomes a synthetic captured task carrying the
|
|
6
|
+
* SKILL.md as its single step, runs through the SAME mandatory scrub
|
|
7
|
+
* (`capture()`) and the SAME publish/anchor path (`publish()`) as a real
|
|
8
|
+
* contribution — one publish path, per the 2026-07-02 schema review — with
|
|
9
|
+
* `provenance: 'imported'` and attribution metadata in the step attributes.
|
|
10
|
+
* Seeds are thereby excluded from the demand signal and from emissions
|
|
11
|
+
* eligibility by every provenance-aware reader.
|
|
12
|
+
*
|
|
13
|
+
* The licence gate is structural, not advisory: execute() re-checks each
|
|
14
|
+
* row's licence from the freshly-fetched source. A verdict hand-edited to
|
|
15
|
+
* `import` over a non-allowlisted licence is refused — widening the gate is
|
|
16
|
+
* a code change to the disclosed allowlist, reviewable in a PR, not a
|
|
17
|
+
* report-file edit.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent by seed identity (issue #1771, `state.ts`): re-running execute
|
|
20
|
+
* over an unchanged skill publishes nothing new (a `skipped` row instead);
|
|
21
|
+
* re-running over a CHANGED skill (different skillMd/description/licence/
|
|
22
|
+
* source) republishes and sets the new record's
|
|
23
|
+
* `provenance.supersedes` to the prior envelopeRef. `opts.state` defaults to
|
|
24
|
+
* a fresh in-memory store per call, so existing callers that never pass one
|
|
25
|
+
* see unchanged behaviour (every row always publishes) — the CLI wires a
|
|
26
|
+
* file-backed store for real runs (cli.ts, `seed execute`).
|
|
27
|
+
*/
|
|
28
|
+
import { capture } from '../capture.js';
|
|
29
|
+
import { buildSeedScrubPipeline } from '@jinn-network/core/scrub';
|
|
30
|
+
import { publish, PublishLedgerError, } from '../publish.js';
|
|
31
|
+
import { checkLicence } from './licence.js';
|
|
32
|
+
import { createMemorySeedImportState, hashSeedContent, } from './state.js';
|
|
33
|
+
/** Caps from the frozen envelope schema (envelope-v0.md size limits). */
|
|
34
|
+
const MAX_TAG_CHARS = 64;
|
|
35
|
+
const MAX_TAGS = 16;
|
|
36
|
+
/**
|
|
37
|
+
* Extract `name:` from a SKILL.md YAML frontmatter block, if present.
|
|
38
|
+
* Deliberately a line-parser, not a YAML dependency — skill frontmatter
|
|
39
|
+
* names are plain scalars; anything more exotic simply yields no tag.
|
|
40
|
+
*/
|
|
41
|
+
export function frontmatterName(skillMd) {
|
|
42
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(skillMd);
|
|
43
|
+
if (!match)
|
|
44
|
+
return null;
|
|
45
|
+
for (const line of match[1].split(/\r?\n/)) {
|
|
46
|
+
const kv = /^name:\s*(.+)$/.exec(line.trim());
|
|
47
|
+
if (kv) {
|
|
48
|
+
const value = kv[1].trim().replace(/^["']|["']$/g, '').trim();
|
|
49
|
+
return value || null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/** Last path segment of a registry-style skill id (`owner/repo/slug`). */
|
|
55
|
+
export function skillSlug(skillId) {
|
|
56
|
+
const segments = skillId.split('/').filter((s) => s.length > 0);
|
|
57
|
+
return segments[segments.length - 1] ?? skillId;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Seed tags (#1343): the seed marker, the repo, the skill slug (last path
|
|
61
|
+
* segment), and the SKILL.md frontmatter name when it declares one. Tags are
|
|
62
|
+
* freeform under the frozen schema (envelope-v0.md: no fixed taxonomy) but
|
|
63
|
+
* the size caps are structural — dedupe, clamp to 64 chars, cap at 16.
|
|
64
|
+
* The signal's authoritative seed marker stays `provenance: 'imported'`;
|
|
65
|
+
* `seed-import` is honest labelling on top.
|
|
66
|
+
*/
|
|
67
|
+
function seedTags(skill) {
|
|
68
|
+
const segments = skill.skill.split('/').filter((s) => s.length > 0);
|
|
69
|
+
const repoName = segments[1] ?? skill.skill;
|
|
70
|
+
const slug = skillSlug(skill.skill);
|
|
71
|
+
const name = frontmatterName(skill.skillMd);
|
|
72
|
+
const tags = [];
|
|
73
|
+
for (const candidate of ['seed-import', repoName, slug, ...(name ? [name] : [])]) {
|
|
74
|
+
const tag = candidate.trim().slice(0, MAX_TAG_CHARS);
|
|
75
|
+
if (tag && !tags.includes(tag))
|
|
76
|
+
tags.push(tag);
|
|
77
|
+
}
|
|
78
|
+
return tags.slice(0, MAX_TAGS);
|
|
79
|
+
}
|
|
80
|
+
function toCapturedTask(skill, now) {
|
|
81
|
+
const nano = `${now.getTime()}000000`;
|
|
82
|
+
return {
|
|
83
|
+
session: {
|
|
84
|
+
sessionId: `seed:${skill.skill}`,
|
|
85
|
+
capturedAt: now.toISOString(),
|
|
86
|
+
},
|
|
87
|
+
task: {
|
|
88
|
+
summary: `Seed import: ${skill.skill}${skill.description ? ` — ${skill.description}` : ''}`,
|
|
89
|
+
distributionTags: seedTags(skill),
|
|
90
|
+
},
|
|
91
|
+
environment: {
|
|
92
|
+
harness: { name: 'jinn-layer-seed-import', version: '0.1.0' },
|
|
93
|
+
model: 'none',
|
|
94
|
+
tools: [],
|
|
95
|
+
},
|
|
96
|
+
steps: [
|
|
97
|
+
{
|
|
98
|
+
spanId: 'seed-1',
|
|
99
|
+
parentSpanId: null,
|
|
100
|
+
name: 'seed:skill-md',
|
|
101
|
+
startTimeUnixNano: nano,
|
|
102
|
+
endTimeUnixNano: nano,
|
|
103
|
+
attributes: {
|
|
104
|
+
'skill.md': skill.skillMd,
|
|
105
|
+
'seed.attribution': {
|
|
106
|
+
skill: skill.skill,
|
|
107
|
+
source: skill.source,
|
|
108
|
+
licence: skill.licence,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
redactedKeys: [],
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
outcome: {
|
|
115
|
+
// user-accepted is literal here: the human approved this exact list.
|
|
116
|
+
status: 'completed',
|
|
117
|
+
verifiabilityTier: 'user-accepted',
|
|
118
|
+
},
|
|
119
|
+
cost: { durationMs: 0 },
|
|
120
|
+
provenance: 'imported',
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** First-class skill artifact for a seed (#1394). Companion-file fetch is #1381. */
|
|
124
|
+
function toSkillArtifact(skill, safeAddress, supersedes) {
|
|
125
|
+
return {
|
|
126
|
+
schemaVersion: 'jinn.skill.v1',
|
|
127
|
+
skill: {
|
|
128
|
+
name: frontmatterName(skill.skillMd) ?? skillSlug(skill.skill),
|
|
129
|
+
...(skill.description !== undefined ? { description: skill.description } : {}),
|
|
130
|
+
skillMd: skill.skillMd,
|
|
131
|
+
},
|
|
132
|
+
files: [],
|
|
133
|
+
provenance: {
|
|
134
|
+
kind: 'imported',
|
|
135
|
+
sourceEnvelopeCids: [],
|
|
136
|
+
operator: { safeAddress },
|
|
137
|
+
seed: { skill: skill.skill, source: skill.source, licence: skill.licence },
|
|
138
|
+
...(supersedes ? { supersedes } : {}),
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** The content fields that define "did this seed change" for idempotency (state.ts). */
|
|
143
|
+
function skillContentFingerprint(skill) {
|
|
144
|
+
return {
|
|
145
|
+
skillMd: skill.skillMd,
|
|
146
|
+
description: skill.description ?? null,
|
|
147
|
+
licence: skill.licence,
|
|
148
|
+
source: skill.source,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
export async function execute(report, source, deps, opts = {}) {
|
|
152
|
+
const skills = new Map((await source.list()).map((s) => [s.skill, s]));
|
|
153
|
+
const result = { imported: [], skipped: [], errors: [] };
|
|
154
|
+
const now = deps.now?.() ?? new Date();
|
|
155
|
+
// Seed profile (#1409): deterministic secret detectors only. Skill seeds in
|
|
156
|
+
// this lane are public, licence-checked prose, not operator trace data — the
|
|
157
|
+
// probabilistic stages false-positive on SKILL.md content and defaced
|
|
158
|
+
// the anchored corpus. Capture stays mandatory and fail-closed.
|
|
159
|
+
const seedScrubPipeline = buildSeedScrubPipeline();
|
|
160
|
+
const state = opts.state ?? createMemorySeedImportState();
|
|
161
|
+
for (const row of report) {
|
|
162
|
+
if (row.verdict === 'skip') {
|
|
163
|
+
result.skipped.push({ skill: row.skill, reason: row.reason });
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const skill = skills.get(row.skill);
|
|
168
|
+
if (!skill)
|
|
169
|
+
throw new Error(`skill ${row.skill} not found in source ${source.name}`);
|
|
170
|
+
const licence = checkLicence(skill.licence);
|
|
171
|
+
if (licence.verdict !== 'import') {
|
|
172
|
+
throw new Error(`licence gate refused ${row.skill}: ${licence.reason}`);
|
|
173
|
+
}
|
|
174
|
+
const identity = `skill:${skill.skill}`;
|
|
175
|
+
const contentHash = hashSeedContent(skillContentFingerprint(skill));
|
|
176
|
+
let prior;
|
|
177
|
+
try {
|
|
178
|
+
prior = state.get(identity);
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
result.errors.push({
|
|
182
|
+
skill: row.skill,
|
|
183
|
+
error: err instanceof Error ? err.message : String(err),
|
|
184
|
+
});
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
if (prior && prior.contentHash === contentHash) {
|
|
188
|
+
result.skipped.push({ skill: row.skill, reason: `unchanged since ${prior.envelopeRef}` });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const pending = await capture(toCapturedTask(skill, now), {
|
|
192
|
+
pipeline: seedScrubPipeline,
|
|
193
|
+
});
|
|
194
|
+
let published;
|
|
195
|
+
let ledgerWarning;
|
|
196
|
+
try {
|
|
197
|
+
published = await publish(pending, deps, {
|
|
198
|
+
skill: toSkillArtifact(skill, deps.participant.safeAddress, prior?.envelopeRef),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
if (err instanceof PublishLedgerError) {
|
|
203
|
+
published = err.result;
|
|
204
|
+
ledgerWarning = err.message;
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
208
|
+
result.errors.push({
|
|
209
|
+
skill: row.skill,
|
|
210
|
+
error: `publication outcome unknown; do not auto-retry: ${detail}`,
|
|
211
|
+
});
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (published.vetoed)
|
|
216
|
+
throw new Error('unexpected veto on seed publish');
|
|
217
|
+
let stateWarning;
|
|
218
|
+
try {
|
|
219
|
+
state.set(identity, { contentHash, envelopeRef: published.envelopeRef, publishedAt: now.toISOString() });
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
const detail = err instanceof Error ? err.message : String(err);
|
|
223
|
+
stateWarning =
|
|
224
|
+
`published ${published.envelopeRef}, but seed-import state persistence failed: ${detail}; ` +
|
|
225
|
+
'recovery required before retrying this seed';
|
|
226
|
+
}
|
|
227
|
+
result.imported.push({
|
|
228
|
+
skill: row.skill,
|
|
229
|
+
envelopeRef: published.envelopeRef,
|
|
230
|
+
anchorTx: published.anchorTx,
|
|
231
|
+
...(ledgerWarning ? { ledgerWarning } : {}),
|
|
232
|
+
...(stateWarning ? { stateWarning } : {}),
|
|
233
|
+
});
|
|
234
|
+
if (ledgerWarning || stateWarning)
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
catch (err) {
|
|
238
|
+
result.errors.push({
|
|
239
|
+
skill: row.skill,
|
|
240
|
+
error: err instanceof Error ? err.message : String(err),
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return result;
|
|
245
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed sources (plan Task 6, spec §7).
|
|
3
|
+
*
|
|
4
|
+
* skills.sh (the first registry, per spec D8) exposes no stable public
|
|
5
|
+
* listing API — its entries are GitHub repos. The durable listing surface is
|
|
6
|
+
* therefore a **disclosed input list** of `owner/repo[#path]` entries (which
|
|
7
|
+
* also matches D9's bounded-allowlist posture: nothing imports that was not
|
|
8
|
+
* on a list a human read), resolved against the GitHub API for the licence
|
|
9
|
+
* (`GET /repos/{owner}/{repo}` → `license.spdx_id`) and the SKILL.md
|
|
10
|
+
* content.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
export interface SeedSkill {
|
|
14
|
+
/** Registry-style id, e.g. `vercel-labs/skills/find-skills`. */
|
|
15
|
+
skill: string;
|
|
16
|
+
/** Canonical source URL (the attribution target). */
|
|
17
|
+
source: string;
|
|
18
|
+
/** SPDX id, or null when the repo declares none. */
|
|
19
|
+
licence: string | null;
|
|
20
|
+
description?: string;
|
|
21
|
+
/** The SKILL.md content (the layer-2 consumable, spec §5). */
|
|
22
|
+
skillMd: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Zod mirror of `SeedSkill` (issue #1771): the wire shape of a skill-shaped
|
|
26
|
+
* seed fixture on disk (e.g. `fixtures/stage1-seeds/distractor-skill-*.json`),
|
|
27
|
+
* used by the fixture-lint test to validate skill-shaped fixtures without a
|
|
28
|
+
* live GitHub source. Not consumed by `createGithubSeedSource` — that source
|
|
29
|
+
* still speaks `SeedSkill` directly.
|
|
30
|
+
*/
|
|
31
|
+
export declare const SeedSkillSchema: z.ZodObject<{
|
|
32
|
+
skill: z.ZodString;
|
|
33
|
+
source: z.ZodString;
|
|
34
|
+
licence: z.ZodNullable<z.ZodString>;
|
|
35
|
+
description: z.ZodOptional<z.ZodString>;
|
|
36
|
+
skillMd: z.ZodString;
|
|
37
|
+
}, z.core.$strict>;
|
|
38
|
+
export interface SeedSource {
|
|
39
|
+
name: string;
|
|
40
|
+
list(): Promise<SeedSkill[]>;
|
|
41
|
+
}
|
|
42
|
+
/** One line of the disclosed input list: `owner/repo` or `owner/repo#sub/path`. */
|
|
43
|
+
export interface SeedListEntry {
|
|
44
|
+
owner: string;
|
|
45
|
+
repo: string;
|
|
46
|
+
/** Path to the skill directory inside the repo (contains SKILL.md). */
|
|
47
|
+
path?: string;
|
|
48
|
+
}
|
|
49
|
+
export declare function parseSeedListEntry(line: string): SeedListEntry;
|
|
50
|
+
export interface GithubSeedSourceOptions {
|
|
51
|
+
/** The disclosed input list. */
|
|
52
|
+
entries: SeedListEntry[];
|
|
53
|
+
/** GitHub token (raises rate limits; optional for small lists). */
|
|
54
|
+
token?: string;
|
|
55
|
+
fetchImpl?: typeof fetch;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* GitHub-backed seed source over a disclosed entry list. Licence comes from
|
|
59
|
+
* the repo's declared licence; SKILL.md from `{path}/SKILL.md` (repo root
|
|
60
|
+
* when no path is given).
|
|
61
|
+
*/
|
|
62
|
+
export declare function createGithubSeedSource(options: GithubSeedSourceOptions): SeedSource;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed sources (plan Task 6, spec §7).
|
|
3
|
+
*
|
|
4
|
+
* skills.sh (the first registry, per spec D8) exposes no stable public
|
|
5
|
+
* listing API — its entries are GitHub repos. The durable listing surface is
|
|
6
|
+
* therefore a **disclosed input list** of `owner/repo[#path]` entries (which
|
|
7
|
+
* also matches D9's bounded-allowlist posture: nothing imports that was not
|
|
8
|
+
* on a list a human read), resolved against the GitHub API for the licence
|
|
9
|
+
* (`GET /repos/{owner}/{repo}` → `license.spdx_id`) and the SKILL.md
|
|
10
|
+
* content.
|
|
11
|
+
*/
|
|
12
|
+
import { z } from 'zod';
|
|
13
|
+
/**
|
|
14
|
+
* Zod mirror of `SeedSkill` (issue #1771): the wire shape of a skill-shaped
|
|
15
|
+
* seed fixture on disk (e.g. `fixtures/stage1-seeds/distractor-skill-*.json`),
|
|
16
|
+
* used by the fixture-lint test to validate skill-shaped fixtures without a
|
|
17
|
+
* live GitHub source. Not consumed by `createGithubSeedSource` — that source
|
|
18
|
+
* still speaks `SeedSkill` directly.
|
|
19
|
+
*/
|
|
20
|
+
export const SeedSkillSchema = z.strictObject({
|
|
21
|
+
skill: z.string().min(1),
|
|
22
|
+
source: z.string().min(1),
|
|
23
|
+
licence: z.string().min(1).nullable(),
|
|
24
|
+
description: z.string().min(1).optional(),
|
|
25
|
+
skillMd: z.string().min(1),
|
|
26
|
+
});
|
|
27
|
+
export function parseSeedListEntry(line) {
|
|
28
|
+
const [repoPart, path] = line.trim().split('#', 2);
|
|
29
|
+
const segments = (repoPart ?? '').split('/').filter((s) => s.length > 0);
|
|
30
|
+
if (segments.length !== 2) {
|
|
31
|
+
throw new Error(`invalid seed list entry "${line}" — expected owner/repo or owner/repo#path`);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
owner: segments[0],
|
|
35
|
+
repo: segments[1],
|
|
36
|
+
...(path ? { path } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const GITHUB_API = 'https://api.github.com';
|
|
40
|
+
async function githubJson(url, token, fetchImpl) {
|
|
41
|
+
const res = await fetchImpl(url, {
|
|
42
|
+
headers: {
|
|
43
|
+
accept: 'application/vnd.github+json',
|
|
44
|
+
'user-agent': 'jinn-harness-layer-seed-import',
|
|
45
|
+
...(token ? { authorization: `Bearer ${token}` } : {}),
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
throw new Error(`GitHub ${res.status} for ${url}`);
|
|
50
|
+
return res.json();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* GitHub-backed seed source over a disclosed entry list. Licence comes from
|
|
54
|
+
* the repo's declared licence; SKILL.md from `{path}/SKILL.md` (repo root
|
|
55
|
+
* when no path is given).
|
|
56
|
+
*/
|
|
57
|
+
export function createGithubSeedSource(options) {
|
|
58
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
59
|
+
return {
|
|
60
|
+
name: 'github-seed-list',
|
|
61
|
+
async list() {
|
|
62
|
+
const skills = [];
|
|
63
|
+
for (const entry of options.entries) {
|
|
64
|
+
const repoUrl = `${GITHUB_API}/repos/${entry.owner}/${entry.repo}`;
|
|
65
|
+
const repo = (await githubJson(repoUrl, options.token, fetchImpl));
|
|
66
|
+
const skillMdPath = entry.path ? `${entry.path}/SKILL.md` : 'SKILL.md';
|
|
67
|
+
const content = (await githubJson(`${repoUrl}/contents/${skillMdPath}`, options.token, fetchImpl));
|
|
68
|
+
if (!content.content || content.encoding !== 'base64') {
|
|
69
|
+
throw new Error(`no SKILL.md at ${entry.owner}/${entry.repo}/${skillMdPath}`);
|
|
70
|
+
}
|
|
71
|
+
skills.push({
|
|
72
|
+
skill: [entry.owner, entry.repo, ...(entry.path ? [entry.path] : [])].join('/'),
|
|
73
|
+
source: repo.html_url ?? `https://github.com/${entry.owner}/${entry.repo}`,
|
|
74
|
+
licence: repo.license?.spdx_id ?? null,
|
|
75
|
+
...(repo.description ? { description: repo.description } : {}),
|
|
76
|
+
skillMd: Buffer.from(content.content, 'base64').toString('utf-8'),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
return skills;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-skill licence gate (plan Task 6, spec §7 / D8).
|
|
3
|
+
*
|
|
4
|
+
* The allowlist is bounded and disclosed: permissive licences import (with
|
|
5
|
+
* attribution carried in the published envelope); anything else — including
|
|
6
|
+
* a missing licence — skips with a stated reason. Copyleft is excluded not
|
|
7
|
+
* as a legal judgement but as a posture: the corpus republishes content, and
|
|
8
|
+
* share-alike terms make that a per-licence analysis we have not done.
|
|
9
|
+
*/
|
|
10
|
+
/** SPDX ids that import (with attribution). Bounded, disclosed. */
|
|
11
|
+
export declare const IMPORT_LICENCE_ALLOWLIST: readonly string[];
|
|
12
|
+
export interface LicenceVerdict {
|
|
13
|
+
verdict: 'import' | 'skip';
|
|
14
|
+
reason: string;
|
|
15
|
+
}
|
|
16
|
+
/** Map an SPDX id (or null = no licence found) to an import verdict. */
|
|
17
|
+
export declare function checkLicence(spdx: string | null | undefined): LicenceVerdict;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-skill licence gate (plan Task 6, spec §7 / D8).
|
|
3
|
+
*
|
|
4
|
+
* The allowlist is bounded and disclosed: permissive licences import (with
|
|
5
|
+
* attribution carried in the published envelope); anything else — including
|
|
6
|
+
* a missing licence — skips with a stated reason. Copyleft is excluded not
|
|
7
|
+
* as a legal judgement but as a posture: the corpus republishes content, and
|
|
8
|
+
* share-alike terms make that a per-licence analysis we have not done.
|
|
9
|
+
*/
|
|
10
|
+
/** SPDX ids that import (with attribution). Bounded, disclosed. */
|
|
11
|
+
export const IMPORT_LICENCE_ALLOWLIST = [
|
|
12
|
+
'MIT',
|
|
13
|
+
'Apache-2.0',
|
|
14
|
+
'BSD-2-Clause',
|
|
15
|
+
'BSD-3-Clause',
|
|
16
|
+
'ISC',
|
|
17
|
+
'CC0-1.0',
|
|
18
|
+
'Unlicense',
|
|
19
|
+
'MIT-0',
|
|
20
|
+
];
|
|
21
|
+
/** Map an SPDX id (or null = no licence found) to an import verdict. */
|
|
22
|
+
export function checkLicence(spdx) {
|
|
23
|
+
if (!spdx || spdx === 'NOASSERTION') {
|
|
24
|
+
return {
|
|
25
|
+
verdict: 'skip',
|
|
26
|
+
reason: 'no licence found — cannot republish without one',
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (IMPORT_LICENCE_ALLOWLIST.includes(spdx)) {
|
|
30
|
+
return { verdict: 'import', reason: `${spdx} — permissive, import with attribution` };
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
verdict: 'skip',
|
|
34
|
+
reason: `${spdx} is not on the import allowlist (permissive licences only)`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `plan()` — fetch + licence-check, ZERO writes (plan Task 6).
|
|
3
|
+
*
|
|
4
|
+
* Takes only a SeedSource; it cannot publish, anchor or touch the ledger by
|
|
5
|
+
* construction (no publish deps in the signature). Its output is the
|
|
6
|
+
* ImportReport the human approves before `execute()` may run.
|
|
7
|
+
*/
|
|
8
|
+
import type { SeedSource } from './fetch.js';
|
|
9
|
+
import type { ImportReport } from './report.js';
|
|
10
|
+
export declare function plan(source: SeedSource): Promise<ImportReport>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `plan()` — fetch + licence-check, ZERO writes (plan Task 6).
|
|
3
|
+
*
|
|
4
|
+
* Takes only a SeedSource; it cannot publish, anchor or touch the ledger by
|
|
5
|
+
* construction (no publish deps in the signature). Its output is the
|
|
6
|
+
* ImportReport the human approves before `execute()` may run.
|
|
7
|
+
*/
|
|
8
|
+
import { checkLicence } from './licence.js';
|
|
9
|
+
export async function plan(source) {
|
|
10
|
+
const skills = await source.list();
|
|
11
|
+
return skills.map((skill) => {
|
|
12
|
+
const { verdict, reason } = checkLicence(skill.licence);
|
|
13
|
+
return {
|
|
14
|
+
skill: skill.skill,
|
|
15
|
+
source: skill.source,
|
|
16
|
+
licence: skill.licence,
|
|
17
|
+
verdict,
|
|
18
|
+
reason,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ImportReport — the approval-gate artifact (plan Task 6).
|
|
3
|
+
*
|
|
4
|
+
* `seed plan` renders and writes this; Oak reads and approves (editing
|
|
5
|
+
* verdicts if needed) BEFORE `seed execute` runs. Nothing lands on chain
|
|
6
|
+
* that was not on this list.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
export declare const ImportReportRowSchema: z.ZodObject<{
|
|
10
|
+
skill: z.ZodString;
|
|
11
|
+
source: z.ZodString;
|
|
12
|
+
licence: z.ZodNullable<z.ZodString>;
|
|
13
|
+
verdict: z.ZodEnum<{
|
|
14
|
+
import: "import";
|
|
15
|
+
skip: "skip";
|
|
16
|
+
}>;
|
|
17
|
+
reason: z.ZodString;
|
|
18
|
+
}, z.core.$strict>;
|
|
19
|
+
export type ImportReportRow = z.infer<typeof ImportReportRowSchema>;
|
|
20
|
+
export declare const ImportReportSchema: z.ZodArray<z.ZodObject<{
|
|
21
|
+
skill: z.ZodString;
|
|
22
|
+
source: z.ZodString;
|
|
23
|
+
licence: z.ZodNullable<z.ZodString>;
|
|
24
|
+
verdict: z.ZodEnum<{
|
|
25
|
+
import: "import";
|
|
26
|
+
skip: "skip";
|
|
27
|
+
}>;
|
|
28
|
+
reason: z.ZodString;
|
|
29
|
+
}, z.core.$strict>>;
|
|
30
|
+
export type ImportReport = z.infer<typeof ImportReportSchema>;
|
|
31
|
+
/** Parse an untrusted value (e.g. an edited report file); throws ZodError. */
|
|
32
|
+
export declare function parseImportReport(input: unknown): ImportReport;
|
|
33
|
+
/** Human-readable report table for the terminal. */
|
|
34
|
+
export declare function renderImportReport(report: ImportReport): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ImportReport — the approval-gate artifact (plan Task 6).
|
|
3
|
+
*
|
|
4
|
+
* `seed plan` renders and writes this; Oak reads and approves (editing
|
|
5
|
+
* verdicts if needed) BEFORE `seed execute` runs. Nothing lands on chain
|
|
6
|
+
* that was not on this list.
|
|
7
|
+
*/
|
|
8
|
+
import { z } from 'zod';
|
|
9
|
+
export const ImportReportRowSchema = z.strictObject({
|
|
10
|
+
skill: z.string().min(1),
|
|
11
|
+
source: z.string().min(1),
|
|
12
|
+
licence: z.string().min(1).nullable(),
|
|
13
|
+
verdict: z.enum(['import', 'skip']),
|
|
14
|
+
reason: z.string().min(1),
|
|
15
|
+
});
|
|
16
|
+
export const ImportReportSchema = z.array(ImportReportRowSchema);
|
|
17
|
+
/** Parse an untrusted value (e.g. an edited report file); throws ZodError. */
|
|
18
|
+
export function parseImportReport(input) {
|
|
19
|
+
return ImportReportSchema.parse(input);
|
|
20
|
+
}
|
|
21
|
+
/** Human-readable report table for the terminal. */
|
|
22
|
+
export function renderImportReport(report) {
|
|
23
|
+
if (report.length === 0)
|
|
24
|
+
return 'Empty report — no skills listed.';
|
|
25
|
+
const lines = [`${report.length} skill(s)`, ''];
|
|
26
|
+
for (const row of report) {
|
|
27
|
+
lines.push(`${row.verdict === 'import' ? 'IMPORT' : 'skip '} ${row.skill}`, ` licence ${row.licence ?? '- (none found)'}`, ` source ${row.source}`, ` reason ${row.reason}`, '');
|
|
28
|
+
}
|
|
29
|
+
lines.pop();
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed-import idempotency state (issue #1771, folding the idempotency AC
|
|
3
|
+
* originally scoped to #1772/R2 — R4 has no dependency on R1 so it ships
|
|
4
|
+
* here; see the #1771 PR body for the rationale).
|
|
5
|
+
*
|
|
6
|
+
* Both seed lanes (skill seeds in `execute.ts`, evidence-episode seeds in
|
|
7
|
+
* `episode-execute.ts`) publish through this shared store: a small
|
|
8
|
+
* identity -> {contentHash, envelopeRef} map that each `execute*()` consults
|
|
9
|
+
* before publishing. Re-running an import over unchanged content is a no-op
|
|
10
|
+
* (a `skipped` row; no new envelope); re-running over CHANGED content
|
|
11
|
+
* republishes and points the new record's provenance at the prior
|
|
12
|
+
* `envelopeRef` via `supersedes` — skills carry it in
|
|
13
|
+
* `SkillArtifactV1.provenance.supersedes` (client/src/types/skill-artifact.ts,
|
|
14
|
+
* already schema'd for this, issue #1462); episodes carry it in the
|
|
15
|
+
* `seed.attribution` step attribute (episode-fetch.ts's step convention),
|
|
16
|
+
* since the frozen `jinn.trace-envelope.v0` has no top-level field for it.
|
|
17
|
+
*
|
|
18
|
+
* File-backed by default (`~/.jinn-client/harness-layer/seed-import-state.json`,
|
|
19
|
+
* mirroring `ledger.ts`'s convention) so real CLI runs persist across
|
|
20
|
+
* invocations; tests inject `createMemorySeedImportState()`.
|
|
21
|
+
*
|
|
22
|
+
* Deliberately NOT a corpus query: idempotency here answers "did *I* already
|
|
23
|
+
* publish this exact seed identity, unchanged?" — a question the operator's
|
|
24
|
+
* own local state answers without a network round trip. It does not detect a
|
|
25
|
+
* differently-identified duplicate already sitting in the shared corpus
|
|
26
|
+
* (two seeds, two identities, same content) — that is the consumer-side
|
|
27
|
+
* content-key dedup (rescope plan §3.3) and the testnet hygiene sweep
|
|
28
|
+
* (#1776), not a publish-time concern.
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
export declare const DEFAULT_SEED_IMPORT_STATE_PATH: string;
|
|
32
|
+
export declare const SeedPublicationRecordSchema: z.ZodObject<{
|
|
33
|
+
contentHash: z.ZodString;
|
|
34
|
+
envelopeRef: z.ZodString;
|
|
35
|
+
publishedAt: z.ZodISODateTime;
|
|
36
|
+
}, z.core.$strict>;
|
|
37
|
+
export type SeedPublicationRecord = z.infer<typeof SeedPublicationRecordSchema>;
|
|
38
|
+
export interface SeedImportStateStore {
|
|
39
|
+
/** The prior publication of this identity, or undefined if never published. */
|
|
40
|
+
get(identity: string): SeedPublicationRecord | undefined;
|
|
41
|
+
/** Record identity's latest publication (overwrites any prior entry). */
|
|
42
|
+
set(identity: string, record: SeedPublicationRecord): void;
|
|
43
|
+
}
|
|
44
|
+
/** In-memory store — tests, and any embedder that does not want on-disk state. */
|
|
45
|
+
export declare function createMemorySeedImportState(initial?: Record<string, SeedPublicationRecord>): SeedImportStateStore;
|
|
46
|
+
export interface FileSeedImportStateOptions {
|
|
47
|
+
/**
|
|
48
|
+
* Optional sink for the fail-closed corrupt-state warning. The read still
|
|
49
|
+
* throws; this hook only lets a CLI surface the same diagnostic separately.
|
|
50
|
+
*/
|
|
51
|
+
onWarning?: (message: string) => void;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* File-backed store (JSON map, `~/.jinn-client/harness-layer/seed-import-state.json`
|
|
55
|
+
* by default) — read-modify-write on every `set()`. Fine at seed-import's
|
|
56
|
+
* scale (dozens to low hundreds of curated, human-approved entries via the
|
|
57
|
+
* plan/execute gate), not a database.
|
|
58
|
+
*
|
|
59
|
+
* A corrupt/foreign state file is FAIL-CLOSED: reads and writes throw rather
|
|
60
|
+
* than treating it as empty and destroying supersedes lineage. Persistence
|
|
61
|
+
* writes a complete same-directory temp file and atomically renames it over
|
|
62
|
+
* the destination, so interruption cannot leave a partial JSON document.
|
|
63
|
+
*/
|
|
64
|
+
export declare function createFileSeedImportState(path?: string, opts?: FileSeedImportStateOptions): SeedImportStateStore;
|
|
65
|
+
/**
|
|
66
|
+
* Deterministic content fingerprint for idempotency: sha256 over the
|
|
67
|
+
* RFC 8785 canonical JSON of the seed's meaningful fields (the caller
|
|
68
|
+
* decides which fields are "meaningful" — e.g. excludes capture-time-only
|
|
69
|
+
* fields like `capturedAt`). Two calls over the same logical content always
|
|
70
|
+
* agree, regardless of key order.
|
|
71
|
+
*/
|
|
72
|
+
export declare function hashSeedContent(value: unknown): string;
|