@6reduk/workspace-pipeline 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.
Files changed (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +78 -0
  3. package/docs/collaboration.md +59 -0
  4. package/docs/config-fields.md +65 -0
  5. package/docs/contracts.md +149 -0
  6. package/docs/doctor.md +165 -0
  7. package/docs/launch.md +50 -0
  8. package/docs/lifecycle-cli.md +141 -0
  9. package/docs/lifecycle.md +42 -0
  10. package/docs/migrations/unity.md +288 -0
  11. package/docs/native-provider-format.md +149 -0
  12. package/docs/provider-bundles.md +81 -0
  13. package/docs/provider-resources.md +37 -0
  14. package/docs/release.md +25 -0
  15. package/docs/remove.md +70 -0
  16. package/docs/repair.md +80 -0
  17. package/docs/repositories.md +209 -0
  18. package/docs/repository-manual-recovery.md +102 -0
  19. package/docs/repository-observations.md +29 -0
  20. package/docs/repository-recovery.md +204 -0
  21. package/docs/repository-retention.md +46 -0
  22. package/docs/repository-transport-budgets.md +26 -0
  23. package/docs/retention.md +237 -0
  24. package/docs/source.md +50 -0
  25. package/docs/switch.md +412 -0
  26. package/package.json +39 -0
  27. package/schemas/common.schema.json +251 -0
  28. package/schemas/inventory.schema.json +15 -0
  29. package/schemas/operation.schema.json +286 -0
  30. package/schemas/pipeline.schema.json +317 -0
  31. package/schemas/state.schema.json +302 -0
  32. package/schemas/workspace.schema.json +67 -0
  33. package/src/cli.js +7 -0
  34. package/src/commands/adopt.js +2 -0
  35. package/src/commands/bootstrap-recovery.js +90 -0
  36. package/src/commands/dispatch.js +394 -0
  37. package/src/commands/init.js +84 -0
  38. package/src/commands/launch.js +69 -0
  39. package/src/commands/migration-apply.js +53 -0
  40. package/src/commands/migration.js +50 -0
  41. package/src/commands/repositories.js +61 -0
  42. package/src/commands/repository-abandon.js +16 -0
  43. package/src/commands/repository-ancestors.js +21 -0
  44. package/src/commands/repository-locks.js +77 -0
  45. package/src/contracts/parse.js +57 -0
  46. package/src/contracts/semantic.js +240 -0
  47. package/src/contracts/validate.js +21 -0
  48. package/src/launch/grok.js +20 -0
  49. package/src/migrations/legacy-unity-begin.js +35 -0
  50. package/src/migrations/legacy-unity-compensate.js +82 -0
  51. package/src/migrations/legacy-unity-deactivate.js +60 -0
  52. package/src/migrations/legacy-unity-deactivation-resume-apply.js +62 -0
  53. package/src/migrations/legacy-unity-deactivation-resume.js +81 -0
  54. package/src/migrations/legacy-unity-finalize.js +88 -0
  55. package/src/migrations/legacy-unity-install-recovery.js +76 -0
  56. package/src/migrations/legacy-unity-install-resume.js +60 -0
  57. package/src/migrations/legacy-unity-install.js +88 -0
  58. package/src/migrations/legacy-unity-lease.js +136 -0
  59. package/src/migrations/legacy-unity-preflight.js +67 -0
  60. package/src/migrations/legacy-unity-preview.js +91 -0
  61. package/src/migrations/legacy-unity-resume-apply.js +39 -0
  62. package/src/migrations/legacy-unity-resume.js +41 -0
  63. package/src/migrations/legacy-unity-resumed-evidence.js +88 -0
  64. package/src/migrations/legacy-unity.js +99 -0
  65. package/src/operations/apply.js +576 -0
  66. package/src/operations/backup.js +94 -0
  67. package/src/operations/bootstrap-lock.js +87 -0
  68. package/src/operations/bootstrap-owner-retirement.js +121 -0
  69. package/src/operations/bundle-update.js +54 -0
  70. package/src/operations/config-fields.js +24 -0
  71. package/src/operations/continuation-lifecycle.js +77 -0
  72. package/src/operations/doctor.js +214 -0
  73. package/src/operations/history.js +170 -0
  74. package/src/operations/installer-identity.js +49 -0
  75. package/src/operations/journal.js +142 -0
  76. package/src/operations/lifecycle.js +133 -0
  77. package/src/operations/lineage-guard.js +20 -0
  78. package/src/operations/lock.js +109 -0
  79. package/src/operations/maintenance.js +62 -0
  80. package/src/operations/migration-pending.js +22 -0
  81. package/src/operations/ownership.js +122 -0
  82. package/src/operations/plan.js +278 -0
  83. package/src/operations/reconciliation.js +112 -0
  84. package/src/operations/recovery-lease.js +66 -0
  85. package/src/operations/remove.js +140 -0
  86. package/src/operations/repair.js +188 -0
  87. package/src/operations/repository-abandon.js +192 -0
  88. package/src/operations/repository-ancestors.js +158 -0
  89. package/src/operations/repository-apply.js +122 -0
  90. package/src/operations/repository-authorization.js +48 -0
  91. package/src/operations/repository-bootstrap-continuation.js +190 -0
  92. package/src/operations/repository-bootstrap-reconcile.js +106 -0
  93. package/src/operations/repository-bootstrap-recover.js +114 -0
  94. package/src/operations/repository-bootstrap.js +82 -0
  95. package/src/operations/repository-clone.js +63 -0
  96. package/src/operations/repository-history.js +108 -0
  97. package/src/operations/repository-inputs.js +41 -0
  98. package/src/operations/repository-journal.js +127 -0
  99. package/src/operations/repository-lock-reconcile.js +401 -0
  100. package/src/operations/repository-pending.js +29 -0
  101. package/src/operations/repository-reconcile.js +182 -0
  102. package/src/operations/repository-resumption-approvals.js +77 -0
  103. package/src/operations/repository-retention-apply.js +75 -0
  104. package/src/operations/repository-retention.js +137 -0
  105. package/src/operations/repository-workspace.js +78 -0
  106. package/src/operations/retention-apply.js +133 -0
  107. package/src/operations/retention-combined-scan.js +30 -0
  108. package/src/operations/retention-combined.js +41 -0
  109. package/src/operations/retention-policy.js +65 -0
  110. package/src/operations/retention-receipts.js +126 -0
  111. package/src/operations/retention-scan.js +86 -0
  112. package/src/operations/retention.js +56 -0
  113. package/src/operations/state.js +210 -0
  114. package/src/operations/switch-activate.js +64 -0
  115. package/src/operations/switch-backups.js +29 -0
  116. package/src/operations/switch-continuation-journal.js +94 -0
  117. package/src/operations/switch-continuation-pending.js +60 -0
  118. package/src/operations/switch-continuation-records.js +109 -0
  119. package/src/operations/switch-continuation-recovery.js +91 -0
  120. package/src/operations/switch-continuation-runtime.js +135 -0
  121. package/src/operations/switch-continuation-store.js +120 -0
  122. package/src/operations/switch-continuation.js +76 -0
  123. package/src/operations/switch-execute.js +68 -0
  124. package/src/operations/switch-inspect.js +45 -0
  125. package/src/operations/switch-journal-store.js +109 -0
  126. package/src/operations/switch-journal.js +71 -0
  127. package/src/operations/switch-lifecycle.js +72 -0
  128. package/src/operations/switch-pending.js +43 -0
  129. package/src/operations/switch-preflight.js +73 -0
  130. package/src/operations/switch-prepare.js +75 -0
  131. package/src/operations/switch-records.js +60 -0
  132. package/src/operations/switch-recovery-store.js +74 -0
  133. package/src/operations/switch.js +52 -0
  134. package/src/operations/toml-fields.js +133 -0
  135. package/src/providers/bundles.js +42 -0
  136. package/src/providers/common-entry.js +16 -0
  137. package/src/providers/grok.js +26 -0
  138. package/src/providers/interface.js +25 -0
  139. package/src/providers/kimi.js +26 -0
  140. package/src/providers/native.js +155 -0
  141. package/src/providers/registry.js +10 -0
  142. package/src/providers/shared.js +51 -0
  143. package/src/providers/source.js +30 -0
  144. package/src/source/git.js +303 -0
  145. package/src/source/inventory.js +87 -0
  146. package/src/source/repository-budget.js +18 -0
  147. package/src/source/snapshot.js +37 -0
  148. package/src/workspace/paths.js +54 -0
  149. package/src/workspace/profiles.js +8 -0
  150. package/src/workspace/repositories.js +57 -0
  151. package/src/workspace/repository-inventory.js +77 -0
  152. package/src/workspace/repository-observation.js +38 -0
  153. package/src/workspace/repository-preflight.js +129 -0
  154. package/src/workspace/repository-preview.js +196 -0
  155. package/src/workspace/repository-tree.js +57 -0
  156. package/src/workspace/reserved.js +11 -0
  157. package/src/workspace/resolve.js +53 -0
@@ -0,0 +1,73 @@
1
+ import path from 'node:path';
2
+ import {validateSwitchRecord} from './switch-records.js';
3
+ import {assertLockHeld} from './lock.js';
4
+ import {prepareRemoval} from './remove.js';
5
+ import {readState,observeTargets,resolveOrigin,verifyPreparedSnapshot} from './state.js';
6
+ import {absoluteRoot,inspectDirectory,resolveChild} from '../workspace/paths.js';
7
+ import {planLayout} from '../workspace/resolve.js';
8
+ import {composePlan,assertRequestScope} from './plan.js';
9
+ import {contractDigest} from '../contracts/semantic.js';
10
+ import {fail} from '../contracts/parse.js';
11
+ import {sha256,cap,LIMITS} from '../source/inventory.js';
12
+
13
+ // Read-only locked preflight. No Git/network, persistence, provider writes or
14
+ // activation. A future executor must repeat relevant checks at write boundaries.
15
+ export async function verifySwitchApproval(lock,prepared,approval,registry,previous) {
16
+ const bound=validateSwitchRecord(prepared,approval,previous),copy=bound.prepared;
17
+ await assertLockHeld(lock);
18
+ if(copy.preview.workspace!==lock.workspace)fail('switch-preflight.workspace');
19
+ const workspace=lock.workspace,plan=copy.preview.phases[1].preview.plan;
20
+ const snapshot={snapshotPath:copy.preparation.snapshot,manifest:{id:plan.desired.pipelineId,version:plan.desired.version},
21
+ digest:plan.source.digest,inventoryDigest:plan.source.inventoryDigest};
22
+ const options={wrapper:workspace,previous:null,manifestPath:plan.source.origin.path,command:'setup'};
23
+ const nested=r=>!r || (!path.isAbsolute(r) && r!=='..' && !r.startsWith('..'+path.sep));
24
+ async function verify() {
25
+ await assertLockHeld(lock);
26
+ for(const candidate of Object.values(copy.preparation)) {
27
+ const root=absoluteRoot(candidate);
28
+ if(nested(path.relative(workspace,root)) || nested(path.relative(root,workspace)))fail('switch-preflight.preparation-location');
29
+ if(!(await inspectDirectory(root)).exists)fail('switch-preflight.preparation-missing');
30
+ }
31
+ const record=await readState(resolveChild(workspace,'.pipeline/state.json'));
32
+ if(record.digest!==copy.stateFileHash || contractDigest(record.value)!==contractDigest(bound.previous))fail('switch-preflight.state-drift');
33
+ // Full old-package replay also verifies installed snapshot and backup bytes.
34
+ if(contractDigest(await prepareRemoval(workspace,registry))!==contractDigest(copy.removal))fail('switch-preflight.removal-drift');
35
+ const origin=await resolveOrigin(options);
36
+ if(contractDigest(origin.origin)!==contractDigest(plan.source.origin) ||
37
+ contractDigest(origin.manifest.pipeline)!==contractDigest(plan.source.source))fail('switch-preflight.origin-drift');
38
+ const verified=await verifyPreparedSnapshot(snapshot);
39
+ const observations=await observeTargets(workspace,copy.preview.observations.map(o=>o.path));
40
+ for(const o of observations) {
41
+ const expected=copy.preview.observations.find(v=>v.path===o.path);
42
+ if((o.bytes===null?null:sha256(o.bytes))!==expected.hash || (o.bytes===null?null:o.bytes.toString('base64'))!==expected.bytes)fail('switch-preflight.observation-drift');
43
+ }
44
+ if((await readState(resolveChild(workspace,'.pipeline/state.json'))).digest!==copy.stateFileHash)fail('switch-preflight.state-drift');
45
+ await assertLockHeld(lock);
46
+ return {origin,verified};
47
+ }
48
+ const {origin,verified}=await verify(),{adapters,sharedAdapter}=registry;
49
+ const layout=planLayout(verified.manifest,origin.manifest,workspace,{adapters});
50
+ const context=()=>({pipeline:structuredClone(verified.manifest),workspace:structuredClone(origin.manifest),layout:structuredClone(layout),snapshot:structuredClone(plan.source),
51
+ files:new Map([...verified.files].map(([name,bytes])=>[name,Buffer.from(bytes)]))});
52
+ const requests=[];
53
+ for(const [owner,adapter] of [['shared',sharedAdapter],...layout.providers.map(id=>[id,adapters[id]])]) {
54
+ if(owner!=='shared' && (await adapter.validate(context()))?.valid===false)fail('provider.validation');
55
+ const batch=await adapter.plan(context());
56
+ if(!Array.isArray(batch))fail('plan.adapter-output');
57
+ cap(requests.length+batch.length,LIMITS.files,'plan.count');
58
+ for(const request of batch) {
59
+ assertRequestScope(request,layout.providers);
60
+ if(request.owner!==owner)fail('plan.adapter-owner');
61
+ requests.push(request.kind==='file'?{...request,bytes:Buffer.isBuffer(request.bytes)?Buffer.from(request.bytes):request.bytes}:structuredClone(request));
62
+ }
63
+ }
64
+ const incoming=copy.preview.phases[1].preview;
65
+ const observations=incoming.observations.map(o=>({path:o.path,bytes:o.bytes===null?null:Buffer.from(o.bytes,'base64')}));
66
+ const rendered=composePlan({pipeline:verified.manifest,workspace:origin.manifest,wrapper:workspace,previous:null,
67
+ snapshot:plan.source,adapters,requests,observations});
68
+ if(contractDigest(rendered)!==contractDigest(incoming) || requests.length!==observations.length ||
69
+ observations.some(o=>!requests.some(r=>r.path===o.path)))fail('switch-preflight.adapter-drift');
70
+ // Async adapters may have changed dependencies: repeat the observations.
71
+ await verify();
72
+ return {...bound,snapshot,applySupported:false,runtime:'not-run'};
73
+ }
@@ -0,0 +1,75 @@
1
+ import path from 'node:path';
2
+ import {tmpdir} from 'node:os';
3
+ import {prepareRemoval} from './remove.js';
4
+ import {composeSwitchPreview} from './switch.js';
5
+ import {composePlan,assertRequestScope} from './plan.js';
6
+ import {readState,resolveOrigin,verifyPreparedSnapshot,observeTargets} from './state.js';
7
+ import {absoluteRoot,inspectDirectory,resolveChild} from '../workspace/paths.js';
8
+ import {planLayout} from '../workspace/resolve.js';
9
+ import {acquire} from '../source/git.js';
10
+ import {contractDigest} from '../contracts/semantic.js';
11
+ import {fail,parse} from '../contracts/parse.js';
12
+ import {requestShape} from './ownership.js';
13
+ import {cap,LIMITS} from '../source/inventory.js';
14
+
15
+ // Staging only: never calls an executor or writes workspace/provider files.
16
+ // Registry is trusted CLI code; the Git package supplies data, not executable code.
17
+ export async function prepareSwitch({wrapper,manifestPath,network=false,tempRoot=tmpdir()},registry) {
18
+ requestShape(registry,['adapters','sharedAdapter'],[],'provider.interface');
19
+ wrapper=absoluteRoot(wrapper);
20
+ await inspectDirectory(wrapper);
21
+ const temporary=(await inspectDirectory(absoluteRoot(tempRoot))).path;
22
+ const rel=path.relative(wrapper,temporary);
23
+ if(!rel || (!rel.startsWith('..'+path.sep) && rel!=='..' && !path.isAbsolute(rel)))fail('plan.preparation-location');
24
+ if(typeof manifestPath!=='string' || !manifestPath.length)fail('switch.manifest-required');
25
+ const removal=await prepareRemoval(wrapper,registry);
26
+ const statePath=resolveChild(wrapper,'.pipeline/state.json'),record=await readState(statePath);
27
+ if(record.digest!==removal.stateFileHash)fail('switch.state-drift');
28
+ const options={wrapper,previous:null,manifestPath,command:'setup'};
29
+ const origin=await resolveOrigin(options);
30
+ const acquired=await acquire(origin.manifest.pipeline,{manifestBase:origin.origin.base,tempRoot:temporary,network});
31
+ if(acquired.resolvedSource!==origin.origin.resolvedSource)fail('source-rebind-required');
32
+ const verified=await verifyPreparedSnapshot(acquired);
33
+ const {adapters,sharedAdapter}=registry;
34
+ const layout=planLayout(verified.manifest,origin.manifest,wrapper,{adapters});
35
+ const snapshot={source:structuredClone(acquired.source),commit:acquired.commit,
36
+ path:'.pipeline/snapshots/'+acquired.digest.slice(7),digest:acquired.digest,
37
+ inventoryDigest:acquired.inventoryDigest,origin:structuredClone(origin.origin)};
38
+ const context=()=>({pipeline:structuredClone(verified.manifest),workspace:structuredClone(origin.manifest),
39
+ layout:structuredClone(layout),snapshot:structuredClone(snapshot),
40
+ files:new Map([...verified.files].map(([name,bytes])=>[name,Buffer.from(bytes)]))});
41
+ const requests=[];
42
+ for(const [owner,adapter] of [['shared',sharedAdapter],...layout.providers.map(id=>[id,adapters[id]])]) {
43
+ if(owner!=='shared' && (await adapter.validate(context()))?.valid===false)fail('provider.validation');
44
+ const batch=await adapter.plan(context());
45
+ if(!Array.isArray(batch))fail('plan.adapter-output');
46
+ cap(requests.length+batch.length,LIMITS.files,'plan.count');
47
+ for(const request of batch) {
48
+ assertRequestScope(request,layout.providers);
49
+ if(request.owner!==owner)fail('plan.adapter-owner');
50
+ // Detach buffers/fields from asynchronous adapter-owned values.
51
+ requests.push(request.kind==='file'?{...request,bytes:Buffer.isBuffer(request.bytes)?Buffer.from(request.bytes):request.bytes}:structuredClone(request));
52
+ }
53
+ }
54
+ planLayout(verified.manifest,origin.manifest,wrapper,{adapters,managedPaths:requests.map(r=>r.path)});
55
+ const observations=await observeTargets(wrapper,requests.map(r=>r.path));
56
+ const removed=new Set(removal.preview.plan.targets.map(t=>t.path));
57
+ const restored=new Map(removal.preview.outputs.map(o=>[o.path,Buffer.from(o.bytes,'base64')]));
58
+ const projected=observations.map(o=>({path:o.path,bytes:removed.has(o.path)?restored.get(o.path)??null:o.bytes}));
59
+ const replacement=composePlan({pipeline:verified.manifest,workspace:origin.manifest,wrapper,
60
+ previous:null,snapshot,adapters,requests,observations:projected});
61
+ const preview=composeSwitchPreview({previous:record.value,removal:removal.preview,replacement});
62
+ // Recheck after all asynchronous adapter work. This is not an atomic read or
63
+ // fresh apply approval; a future locked executor must repeat its preflight.
64
+ if((await prepareRemoval(wrapper,registry)).digest!==removal.digest)fail('switch.removal-drift');
65
+ if(contractDigest(await resolveOrigin(options))!==contractDigest(origin))fail('plan.origin-drift');
66
+ await verifyPreparedSnapshot(acquired);
67
+ const after=await observeTargets(wrapper,observations.map(o=>o.path));
68
+ if(contractDigest(after)!==contractDigest(observations))fail('switch.observation-drift');
69
+ if((await readState(statePath)).digest!==record.digest)fail('switch.state-drift');
70
+ const body={kind:'prepared-switch',preview,removal,stateFileHash:record.digest,
71
+ preparation:{objects:acquired.preparation,snapshot:acquired.snapshotPath},
72
+ sourceVerification:'verified-at-preparation',applySupported:false,requiresFreshApproval:true,runtime:'not-run'};
73
+ const copy=parse(JSON.stringify(body),'json');
74
+ return {...copy,digest:contractDigest(copy)};
75
+ }
@@ -0,0 +1,60 @@
1
+ import {parse,fail} from '../contracts/parse.js';
2
+ import {requestShape} from './ownership.js';
3
+ import {contractDigest,validateState,portablePath} from '../contracts/semantic.js';
4
+ import {composeSwitchPreview} from './switch.js';
5
+ import {absoluteRoot} from '../workspace/paths.js';
6
+
7
+ const hash=value=>typeof value==='string' && /^sha256:[a-f0-9]{64}(?![\s\S])/.test(value);
8
+ const detach=value=>parse(JSON.stringify(value),'json');
9
+
10
+ // Structural binding only: never a substitute for locked replay/source checks.
11
+ export function validateSwitchRecord(prepared,approval,previous) {
12
+ const copy=detach(prepared),decision=detach(approval),state=detach(previous);
13
+ requestShape(copy,['kind','preview','removal','stateFileHash','preparation','sourceVerification','applySupported','requiresFreshApproval','runtime','digest'],[],'switch-record.prepared');
14
+ requestShape(decision,['decision','preparedDigest'],[],'switch-record.approval');
15
+ validateState(state);
16
+ if(state.status!=='ready' || !state.active || state.pending!==null)fail('switch-record.previous');
17
+ const {digest,...body}=copy;
18
+ if(copy.kind!=='prepared-switch' || copy.sourceVerification!=='verified-at-preparation' || copy.applySupported!==false ||
19
+ copy.requiresFreshApproval!==true || copy.runtime!=='not-run' || !hash(copy.stateFileHash) || contractDigest(body)!==digest)fail('switch-record.prepared');
20
+ if(decision.decision!=='approve' || decision.preparedDigest!==digest)fail('switch-record.approval');
21
+ requestShape(copy.preparation,['objects','snapshot'],[],'switch-record.preparation');
22
+ absoluteRoot(copy.preparation.objects);absoluteRoot(copy.preparation.snapshot);
23
+ const r=copy.removal;
24
+ requestShape(r,['kind','preview','stateFileHash','providers','backups','applySupported','requiresFreshApproval','automaticActions','runtime','digest'],['bundles'],'switch-record.removal');
25
+ const {digest:rd,...rb}=r;
26
+ if(r.kind!=='prepared-removal' || r.applySupported!==true || r.requiresFreshApproval!==true || r.automaticActions!==false ||
27
+ r.runtime!=='not-run' || r.stateFileHash!==copy.stateFileHash || contractDigest(rb)!==rd ||
28
+ !Array.isArray(r.providers) || contractDigest(r.providers)!==contractDigest([...state.active.providers].sort()))fail('switch-record.removal');
29
+ if(!Array.isArray(r.backups))fail('switch-record.backups');
30
+ const expected=new Set(state.active.owned.map(o=>o.backup).filter(v=>v!==null)),seen=new Set();
31
+ for(const b of r.backups) {
32
+ requestShape(b,['path','hash'],[],'switch-record.backups');portablePath(b.path);
33
+ if(!expected.has(b.path) || seen.has(b.path) || !hash(b.hash))fail('switch-record.backups');
34
+ seen.add(b.path);
35
+ }
36
+ if(seen.size!==expected.size)fail('switch-record.backups');
37
+ if(!Array.isArray(copy.preview?.phases) || copy.preview.phases.length!==2)fail('switch-record.phases');
38
+ const fresh=composeSwitchPreview({previous:state,removal:r.preview,replacement:copy.preview.phases[1].preview});
39
+ if(contractDigest(fresh)!==contractDigest(copy.preview))fail('switch-record.phases');
40
+ return {prepared:copy,approval:decision,previous:state};
41
+ }
42
+
43
+ export function createSwitchRecoveryRecord({prepared,approval,previous,journal}) {
44
+ const bound=validateSwitchRecord(prepared,approval,previous);
45
+ if(typeof journal!=='string' || !/^\.pipeline\/journals\/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}(?![\s\S])/.test(journal))fail('switch-record.journal');
46
+ // Both sources are required for recovery; the new snapshot must be durably
47
+ // installed and verified before a future executor writes provider targets.
48
+ const body={schemaVersion:1,kind:'switch-recovery',...bound,journal,
49
+ snapshots:{old:bound.previous.active.snapshot,new:bound.prepared.preview.phases[1].preview.plan.source},
50
+ activationSupported:false,runtime:'not-run'};
51
+ return detach({...body,digest:contractDigest(body)});
52
+ }
53
+
54
+ export function validateSwitchRecoveryRecord(record) {
55
+ const copy=detach(record);
56
+ requestShape(copy,['schemaVersion','kind','prepared','approval','previous','journal','snapshots','activationSupported','runtime','digest'],[],'switch-record.recovery');
57
+ const fresh=createSwitchRecoveryRecord(copy);
58
+ if(contractDigest(copy)!==contractDigest(fresh))fail('switch-record.recovery');
59
+ return fresh;
60
+ }
@@ -0,0 +1,74 @@
1
+ import {mkdir,open} from 'node:fs/promises';
2
+ import {verifySwitchApproval} from './switch-preflight.js';
3
+ import {createSwitchRecoveryRecord,validateSwitchRecoveryRecord} from './switch-records.js';
4
+ import {createSwitchJournal,readSwitchJournal} from './switch-journal-store.js';
5
+ import {copySnapshot,saveBackup} from './backup.js';
6
+ import {incomingSwitchBackups} from './switch-backups.js';
7
+ import {assertLockHeld} from './lock.js';
8
+ import {readRecord,observeTargets,verifyPreparedSnapshot,verifyInstalledSnapshot} from './state.js';
9
+ import {resolveChild,inspectDirectory} from '../workspace/paths.js';
10
+ import {sha256} from '../source/inventory.js';
11
+ import {fail,ContractError} from '../contracts/parse.js';
12
+
13
+ // Read-only structural/durable evidence inspection, not current-target recovery
14
+ // or permission to continue. Generic history integration is still pending.
15
+ export async function readSwitchRecovery(workspace,relative) {
16
+ const match=typeof relative==='string' && /^\.pipeline\/transactions\/([a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12})\/recovery.json(?![\s\S])/.exec(relative);
17
+ if(!match)fail('switch-recovery.path');
18
+ const stored=await readRecord(resolveChild(workspace,relative)),record=validateSwitchRecoveryRecord(stored.value);
19
+ if(record.previous.workspace!==workspace || record.journal!=='.pipeline/journals/'+match[1])fail('switch-recovery.binding');
20
+ await verifyInstalledSnapshot(record.previous);
21
+ for(const backup of [...record.prepared.removal.backups,...incomingSwitchBackups(record.prepared.preview.phases[1].preview)]) {
22
+ const [observation]=await observeTargets(workspace,[backup.path]);
23
+ if(observation.bytes===null || sha256(observation.bytes)!==backup.hash)fail('switch-recovery.backup');
24
+ }
25
+ const desired=record.prepared.preview.phases[1].preview.plan.desired;
26
+ await verifyPreparedSnapshot({snapshotPath:resolveChild(workspace,record.snapshots.new.path),
27
+ manifest:{id:desired.pipelineId,version:desired.version},digest:record.snapshots.new.digest,inventoryDigest:record.snapshots.new.inventoryDigest});
28
+ const journal=await readSwitchJournal(workspace,record.journal,record.prepared.preview,record.previous);
29
+ if((await readRecord(resolveChild(workspace,relative))).digest!==stored.digest)fail('switch-recovery.drift');
30
+ return {record,fileHash:stored.digest,journal,applySupported:false,runtime:'not-run'};
31
+ }
32
+
33
+ // Persist prerequisites only. Never changes active/pending state or target files.
34
+ // Orphan/partial metadata is retained on failure, never retried or cleaned here.
35
+ export async function persistSwitchRecovery(lock,prepared,approval,registry,previous,{boundary=async()=>{},onJournal=async()=>{}}={}) {
36
+ const checked=await verifySwitchApproval(lock,prepared,approval,registry,previous);
37
+ const args={prepared:checked.prepared,approval:checked.approval,previous:checked.previous};
38
+ // Bound serialization before any additional metadata write.
39
+ createSwitchRecoveryRecord({...args,journal:'.pipeline/journals/00000000-0000-0000-0000-000000000000'});
40
+ const backups=incomingSwitchBackups(args.prepared.preview.phases[1].preview);
41
+ // Check all existing collisions before saving any new backup. Old backups are
42
+ // immutable; a shared path with different bytes is a blocker, not an overwrite.
43
+ for(const backup of backups) {
44
+ const [current]=await observeTargets(lock.workspace,[backup.path]);
45
+ if(current.bytes!==null && sha256(current.bytes)!==backup.hash)fail('switch-recovery.backup-conflict');
46
+ }
47
+ await copySnapshot(lock,checked.snapshot);
48
+ await boundary('snapshot');
49
+ await verifySwitchApproval(lock,args.prepared,args.approval,registry,args.previous);
50
+ for(const backup of backups)await saveBackup(lock,backup.path,backup.bytes,backup.hash);
51
+ await boundary('backups');
52
+ await verifySwitchApproval(lock,args.prepared,args.approval,registry,args.previous);
53
+ const journal=await createSwitchJournal(lock,args.prepared.preview,args.previous,{onLocation:onJournal});
54
+ const uuid=journal.relative.split('/').at(-1),relative='.pipeline/transactions/'+uuid+'/recovery.json';
55
+ const record=createSwitchRecoveryRecord({...args,journal:journal.relative});
56
+ const bytes=Buffer.from(JSON.stringify(record)+'\n');
57
+ try {
58
+ await assertLockHeld(lock);
59
+ const parent=resolveChild(lock.workspace,'.pipeline/transactions'),directory=resolveChild(lock.workspace,'.pipeline/transactions/'+uuid);
60
+ try{await mkdir(parent);}catch(error){if(error.code!=='EEXIST')throw error;}
61
+ await inspectDirectory(parent);await mkdir(directory);await inspectDirectory(directory);
62
+ const handle=await open(resolveChild(lock.workspace,relative),'wx',0o600);
63
+ try{await handle.writeFile(bytes);await handle.sync();}finally{await handle.close();}
64
+ await boundary('recovery-written',{path:relative});
65
+ if((await readRecord(resolveChild(lock.workspace,relative))).digest!==sha256(bytes))fail('switch-recovery.readback');
66
+ await verifySwitchApproval(lock,args.prepared,args.approval,registry,args.previous);
67
+ const evidence=await readSwitchRecovery(lock.workspace,relative);
68
+ if(evidence.journal.sequence!==1 || evidence.journal.status!=='open')fail('switch-recovery.journal');
69
+ await assertLockHeld(lock);
70
+ // No mutable journal writer is returned: no provider operation is authorized.
71
+ return {recoveryPath:relative,recoveryHash:evidence.fileHash,journalPath:journal.relative,
72
+ snapshotPath:record.snapshots.new.path,applySupported:false,runtime:'not-run'};
73
+ }catch(error){throw error instanceof ContractError?error:new ContractError('switch-recovery.io');}
74
+ }
@@ -0,0 +1,52 @@
1
+ import {checkPreview} from './plan.js';
2
+ import {contractDigest,validateState} from '../contracts/semantic.js';
3
+ import {requestShape} from './ownership.js';
4
+ import {fail,parse} from '../contracts/parse.js';
5
+
6
+ const decode=items=>items.map(o=>({path:o.path,bytes:o.bytes===null?null:Buffer.from(o.bytes,'base64')}));
7
+
8
+ // Pure phase composition, NOT a source verifier, write grant or executable plan.
9
+ // The caller must stage/verify the incoming Git package and replay both registries
10
+ // before obtaining an approval for any future switch executor.
11
+ export function composeSwitchPreview({previous,removal,replacement}) {
12
+ validateState(previous);
13
+ if(previous.status!=='ready' || !previous.active || previous.pending!==null)fail('switch.previous');
14
+ requestShape(removal,['schemaVersion','plan','observations','outputs','digest'],[],'switch.preview');
15
+ requestShape(replacement,['schemaVersion','plan','observations','outputs','digest'],[],'switch.preview');
16
+ const old=checkPreview(removal,previous,decode(removal.observations));
17
+ const incoming=checkPreview(replacement,null,decode(replacement.observations));
18
+ if(old.plan.command!=='remove' || old.plan.desired!==null || old.plan.workspace!==previous.workspace ||
19
+ contractDigest(old.plan.source)!==contractDigest(previous.active.snapshot))fail('switch.removal');
20
+ if(incoming.plan.command!=='setup' || incoming.plan.workspace!==previous.workspace || !incoming.plan.desired)fail('switch.replacement');
21
+ if(incoming.plan.desired.pipelineId===previous.active.pipelineId)fail('switch.same-pipeline');
22
+ if(incoming.plan.source.digest===previous.active.snapshot.digest)fail('switch.source-identity');
23
+ if(contractDigest(incoming.plan.desired.layout)!==contractDigest(previous.active.layout))fail('switch.layout-change');
24
+
25
+ const initial=new Map(old.observations.map(o=>[o.path,o]));
26
+ const removed=new Map(old.plan.targets.map(t=>[t.path,t.desiredHash]));
27
+ const restored=new Map(old.outputs.map(o=>[o.path,o]));
28
+ for(const observation of incoming.observations) {
29
+ if([...initial.keys()].some(name=>name!==observation.path && name.toLowerCase()===observation.path.toLowerCase()))fail('switch.path-alias');
30
+ if(initial.has(observation.path)) {
31
+ const expected=removed.has(observation.path)?removed.get(observation.path):initial.get(observation.path).hash;
32
+ if(observation.hash!==expected)fail('switch.projected-before');
33
+ const expectedBytes=removed.has(observation.path)?restored.get(observation.path)?.bytes??null:initial.get(observation.path).bytes;
34
+ if(observation.bytes!==expectedBytes)fail('switch.projected-before');
35
+ }else initial.set(observation.path,observation);
36
+ }
37
+ // All old managed scope must have been observed even when the incoming package
38
+ // no longer mentions it; its removal must never depend on new adapter coverage.
39
+ if(previous.active.owned.some(o=>!old.observations.some(v=>v.path===o.path)))fail('switch.old-coverage');
40
+ const final=new Map(initial);
41
+ for(const phase of [old,incoming]) {
42
+ const outputs=new Map(phase.outputs.map(o=>[o.path,o]));
43
+ for(const t of phase.plan.targets)final.set(t.path,outputs.get(t.path)??{path:t.path,hash:null,bytes:null});
44
+ }
45
+ const body={kind:'switch-phase-preview',workspace:previous.workspace,beforeStateHash:contractDigest(previous),
46
+ fromPipeline:previous.active.pipelineId,toPipeline:incoming.plan.desired.pipelineId,
47
+ phases:[{name:'remove-old',preview:old},{name:'install-new',preview:incoming}],
48
+ observations:[...initial.values()].sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0),
49
+ results:[...final.values()].sort((a,b)=>a.path<b.path?-1:a.path>b.path?1:0),
50
+ applySupported:false,requiresFreshApproval:true,sourceVerification:'not-verified',runtime:'not-run'};
51
+ const copy=parse(JSON.stringify(body),'json');return {...copy,digest:contractDigest(copy)};
52
+ }
@@ -0,0 +1,133 @@
1
+ import {parseTOML, getStaticTOMLValue} from 'toml-eslint-parser';
2
+ import {isDeepStrictEqual} from 'node:util';
3
+ import {fail, MAX_INPUT_BYTES} from '../contracts/parse.js';
4
+ import {utf8} from '../source/inventory.js';
5
+ import {reconcileFields,requestShape} from './ownership.js';
6
+
7
+ // Only complete named Codex agent/MCP entries. No model, permission, auth or
8
+ // trust setting may be addressed through this editor. No I/O or authorization.
9
+ function parts(pointer) {
10
+ if(typeof pointer!=='string' || !/^\/(agents|mcp_servers)\/[a-z][a-z0-9_-]{0,99}$/.test(pointer))fail('toml.scope');
11
+ const result=pointer.slice(1).split('/');
12
+ if(['__proto__','prototype','constructor'].includes(result[1]))fail('toml.scope');
13
+ if(result[0]==='agents' && ['enabled','max_threads','max_depth','max_concurrent_threads_per_session',
14
+ 'default_subagent_model','default_subagent_reasoning_effort','job_max_runtime_seconds','interrupt_message'].includes(result[1]))fail('toml.scope');
15
+ return result;
16
+ }
17
+ const prefix=(a,b)=>a.length<=b.length && a.every((x,i)=>x===b[i]);
18
+ const keys=key=>key.keys.map(k=>k.type==='TOMLBare'?k.name:k.value);
19
+
20
+ function document(bytes) {
21
+ if(bytes!==null && !Buffer.isBuffer(bytes))fail('toml.input');
22
+ if(bytes!==null && bytes.length>MAX_INPUT_BYTES)fail('toml.size');
23
+ const text=bytes===null?'':utf8(bytes);
24
+ let ast;try{ast=parseTOML(text,{tomlVersion:'1.0'});}catch{fail('toml.syntax');}
25
+ let count=0;
26
+ function inspect(node,depth=0) {
27
+ if(++count>50000 || depth>64)fail('toml.complexity');
28
+ if(node.type==='TOMLKey' && keys(node).some(k=>['__proto__','prototype','constructor'].includes(k)))fail('toml.key');
29
+ // Deliberately omit parent/tokens/comments, which contain cycles or duplicate nodes.
30
+ for(const key of ['body','key','value','elements','keys']) {
31
+ const value=node[key];
32
+ for(const child of Array.isArray(value)?value:[value])if(child && typeof child==='object' && typeof child.type==='string')inspect(child,depth+1);
33
+ }
34
+ }
35
+ inspect(ast);
36
+ let value;try{value=getStaticTOMLValue(ast);}catch{fail('toml.syntax');}
37
+ return {text,ast,value};
38
+ }
39
+
40
+ function selected(value,pointer) {
41
+ const [root,name]=parts(pointer);
42
+ if(!Object.hasOwn(value,root))return {present:false};
43
+ const parent=value[root];
44
+ if(parent===null || typeof parent!=='object' || Array.isArray(parent) || parent instanceof Date)fail('toml.ancestor');
45
+ if(!Object.hasOwn(parent,name))return {present:false};
46
+ const entry=parent[name];
47
+ if(entry===null || typeof entry!=='object' || Array.isArray(entry) || entry instanceof Date)fail('toml.entry');
48
+ // Reuse the JSON-domain validator without admitting dates/NaN/unsafe integers
49
+ // into owned hashes. Foreign TOML values are never serialized or normalized.
50
+ const checked=reconcileFields({},[{pointer,present:true,value:entry}]);
51
+ return {present:true,value:checked.value[root][name]};
52
+ }
53
+
54
+ export function readTOMLField(bytes,pointer) {
55
+ return selected(document(bytes).value,pointer);
56
+ }
57
+
58
+ function literal(value) {
59
+ if(typeof value==='string')return JSON.stringify(value).replace(/\u007f/g,'\\u007f');
60
+ if(typeof value==='boolean' || (typeof value==='number' && Number.isFinite(value) && (!Number.isInteger(value)||Number.isSafeInteger(value))))return String(value);
61
+ if(Array.isArray(value))return '['+value.map(literal).join(', ')+']';
62
+ if(value && typeof value==='object')return '{ '+Object.keys(value).sort().map(k=>JSON.stringify(k)+' = '+literal(value[k])).join(', ')+' }';
63
+ fail('toml.value'); // TOML has no null; no coercion to string/empty value.
64
+ }
65
+
66
+ // Select source spans by parsed semantic keys, never regex matching source text.
67
+ // Inline ancestor tables cannot be split without editing foreign bytes: refuse.
68
+ function deletionRanges(ast,target) {
69
+ const ranges=[];
70
+ const visitPair=(node,base)=>{
71
+ const key=[...base,...keys(node.key)];
72
+ if(prefix(target,key))ranges.push(node.range);
73
+ else if(prefix(key,target))fail('toml.inline-ancestor');
74
+ };
75
+ for(const node of ast.body[0].body) {
76
+ if(node.type==='TOMLKeyValue')visitPair(node,[]);
77
+ else if(node.type==='TOMLTable') {
78
+ const key=node.resolvedKey;
79
+ if(key.some(k=>typeof k!=='string') && prefix(key.filter(k=>typeof k==='string'),target))fail('toml.array-ancestor');
80
+ if(prefix(target,key)) {
81
+ if(node.kind!=='standard')fail('toml.array-ancestor');
82
+ const end=ast.tokens.find(t=>t.range[0]>=node.key.range[1] && t.value===']');
83
+ if(!end)fail('toml.syntax');
84
+ ranges.push([node.range[0],end.range[1]]);
85
+ for(const pair of node.body)ranges.push(pair.range);
86
+ }else for(const pair of node.body)visitPair(pair,key);
87
+ }
88
+ }
89
+ return ranges;
90
+ }
91
+
92
+ function normalizeEmptyRoots(value) {
93
+ for(const root of ['agents','mcp_servers'])if(value[root] && !Array.isArray(value[root]) && Object.keys(value[root]).length===0)delete value[root];
94
+ return value;
95
+ }
96
+
97
+ export function reconcileTOMLFields(bytes,requests) {
98
+ const before=document(bytes),projected=Object.create(null);
99
+ if(!Array.isArray(requests) || !requests.length || requests.length>1000)fail('toml.requests');
100
+ for(const request of requests) {
101
+ requestShape(request,['pointer','present'],['value','managedHash','takeover'],'toml.requests');
102
+ const [root,name]=parts(request?.pointer),found=selected(before.value,request.pointer);
103
+ if(found.present){projected[root]??=Object.create(null);projected[root][name]=found.value;}
104
+ }
105
+ const reconciled=reconcileFields(projected,requests),ranges=[],additions=[],expected=structuredClone(before.value);
106
+ const newline=before.text.includes('\r\n')?'\r\n':'\n';
107
+ for(const decision of reconciled.decisions) {
108
+ if(decision.action==='preserve')continue;
109
+ const target=parts(decision.pointer),[root,name]=target;
110
+ ranges.push(...deletionRanges(before.ast,target));
111
+ const found=selected(reconciled.value,decision.pointer);
112
+ if(found.present) {
113
+ expected[root]??={};expected[root][name]=JSON.parse(JSON.stringify(found.value));
114
+ additions.push('['+target.map(k=>JSON.stringify(k)).join('.')+']'+newline+
115
+ Object.keys(found.value).sort().map(k=>JSON.stringify(k)+' = '+literal(found.value[k])+newline).join(''));
116
+ }else if(expected[root])delete expected[root][name];
117
+ }
118
+ let result=before.text;
119
+ // Consume only the immediate line ending of an owned statement. Never trim
120
+ // whitespace/comments or blank lines outside its AST range.
121
+ const ordered=ranges.map(([start,end])=>[start,end+(before.text.startsWith('\r\n',end)?2:before.text[end]==='\n'?1:0)]).sort((a,b)=>b[0]-a[0]);
122
+ for(let i=0;i<ordered.length;i++) {
123
+ const [start,end]=ordered[i];
124
+ if(i && end>ordered[i-1][0])fail('toml.overlap');
125
+ result=result.slice(0,start)+result.slice(end);
126
+ }
127
+ if(additions.length)result+=(result && !result.endsWith('\n')?newline:'')+additions.join('');
128
+ const output=Buffer.from(result),after=document(output);
129
+ // Compare full semantics, including foreign Date/NaN/Infinity values. Only
130
+ // empty implicit root tables may disappear after their last owned child.
131
+ if(!isDeepStrictEqual(normalizeEmptyRoots(after.value),normalizeEmptyRoots(expected)))fail('toml.postcondition');
132
+ return {bytes:output,decisions:reconciled.decisions};
133
+ }
@@ -0,0 +1,42 @@
1
+ import { fail } from '../contracts/parse.js';
2
+
3
+ // Declarative membership only. Native formats and destination roots stay in
4
+ // trusted compiled adapters; packages cannot provide executable adapters.
5
+ export function resolveProviders(pipeline, workspace) {
6
+ const assigned = new Set();
7
+ for (const bundle of Object.values(pipeline.bundles ?? {})) {
8
+ for (const id of bundle.providers) {
9
+ if (!Object.hasOwn(pipeline.providers, id)) fail('bundle.provider-absent');
10
+ if (assigned.has(id)) fail('bundle.overlap');
11
+ assigned.add(id);
12
+ }
13
+ }
14
+ const providers = new Set(workspace.providers ?? []), bundles = {};
15
+ for (const key of workspace.bundles ?? []) {
16
+ const bundle = Object.hasOwn(pipeline.bundles??{},key) ? pipeline.bundles[key] : undefined;
17
+ if (!bundle) fail('bundle.absent');
18
+ for (const id of bundle.providers) {
19
+ if (providers.has(id)) fail('bundle.selection-overlap');
20
+ providers.add(id);
21
+ }
22
+ bundles[key] = structuredClone(bundle);
23
+ }
24
+ // A selected standalone member of a declared bundle would create a second
25
+ // lifecycle for the same entry. Old sources without bundles still work.
26
+ if ((workspace.providers ?? []).some(id => assigned.has(id))) fail('bundle.select-bundle');
27
+ return { providers: [...providers].sort(), ...(Object.keys(bundles).length ? { bundles } : {}) };
28
+ }
29
+
30
+ export function installedSelection(active) {
31
+ const members = new Set(Object.values(active.bundles ?? {}).flatMap(b => b.providers));
32
+ return { providers: active.providers.filter(id => !members.has(id)),
33
+ ...(active.bundles ? { bundles: Object.keys(active.bundles).sort() } : {}) };
34
+ }
35
+
36
+ export function validateInstalledBundles(active) {
37
+ const seen = new Set();
38
+ for (const bundle of Object.values(active.bundles ?? {})) for (const id of bundle.providers) {
39
+ if (seen.has(id) || !active.providers.includes(id)) fail('bundle.state-membership');
40
+ seen.add(id);
41
+ }
42
+ }
@@ -0,0 +1,16 @@
1
+ // Trusted delivery policy, not source-package instructions or runtime isolation.
2
+ export const commonEntryPath = 'CLAUDE.md';
3
+ export const commonEntryText = '@AGENTS.md\n\nRead [AGENTS.md](AGENTS.md) before acting.\nFollow only the provider instructions for your current harness.\nThis shared entry grants no approval and does not enable another provider.\n';
4
+ export const needsCommonEntry = providers => providers.some(id => id === 'claude' || id === 'grok');
5
+
6
+ // Replay old installed bytes for doctor/repair/remove. Fresh plans always use
7
+ // the current format. Adapter versions are installer metadata, not task gates.
8
+ export const legacyEntryReplay = context => context.installedAdapterVersions !== undefined &&
9
+ ['claude', 'grok'].filter(id => context.workspace.providers.includes(id))
10
+ .every(id => context.installedAdapterVersions[id] === '1');
11
+
12
+ export function removedWithProviders(owned, selection) {
13
+ if (owned.owner === 'shared' && owned.path === commonEntryPath)
14
+ return !needsCommonEntry(selection.remaining);
15
+ return selection.owners.includes(owned.owner);
16
+ }
@@ -0,0 +1,26 @@
1
+ import { fail, MAX_INPUT_BYTES } from '../contracts/parse.js';
2
+ import { sourceText } from './source.js';
3
+ import { skills, agents, mcp } from './native.js';
4
+
5
+ // Direct component rendering only. Compatibility isolation requires the separate
6
+ // launch path; installing these files does not certify an ordinary `grok` launch.
7
+ const capabilities = Object.freeze(['skills', 'agents', 'mcp', 'entry-instructions']);
8
+ function render(context) {
9
+ const declaration = context.pipeline.providers.grok;
10
+ if (!declaration || declaration.requires.some(value => !capabilities.includes(value))) fail('provider.unsupported');
11
+ const requests = [];
12
+ if (declaration.skills !== null) requests.push(...skills(context, 'grok', declaration.skills));
13
+ if (declaration.agents !== null) requests.push(...agents(context, 'grok', declaration.agents).requests);
14
+ if (declaration.mcp !== null) requests.push({ owner: 'grok', path: '.grok/config.toml',
15
+ kind: 'toml-fields', fields: mcp(context, 'grok', declaration.mcp) });
16
+ // The common AGENTS.md owner supplies the provider instruction link.
17
+ if (declaration.entryInstructions !== null) {
18
+ const value = sourceText(context, declaration.entryInstructions);
19
+ if (!value.trim() || value.includes('\0') || Buffer.byteLength(value) > MAX_INPUT_BYTES) fail('provider.format');
20
+ }
21
+ return requests;
22
+ }
23
+ export const grokAdapter = Object.freeze({ id: 'grok', version: '2', capabilities,
24
+ async validate(context) { render(context); return { valid: true, runtime: 'not-run' }; },
25
+ async plan(context) { return render(context); },
26
+ });
@@ -0,0 +1,25 @@
1
+ import { fail } from '../contracts/parse.js';
2
+
3
+ export const CAPABILITIES = Object.freeze(['skills', 'agents', 'mcp', 'entry-instructions', 'compatibility-isolation']);
4
+ export const COMPONENTS = Object.freeze({ skills: 'skills', agents: 'agents', mcp: 'mcp', entryInstructions: 'entry-instructions' });
5
+ export function requiredCapabilities(declaration) {
6
+ const required = new Set(declaration.requires);
7
+ let components = 0;
8
+ for (const [field, capability] of Object.entries(COMPONENTS)) {
9
+ if (declaration[field] !== null) { required.add(capability); components++; }
10
+ else if (required.has(capability)) fail('provider.required-null');
11
+ }
12
+ if (components === 0) fail('provider.empty');
13
+ return [...required].sort();
14
+ }
15
+ // Trusted CLI adapters only. Never dynamically import code from a pipeline package.
16
+ export function assertAdapter(adapter, id, declaration) {
17
+ if (!adapter || adapter.id !== id || typeof adapter.version !== 'string' || !adapter.version ||
18
+ !Array.isArray(adapter.capabilities) ||
19
+ adapter.capabilities.some(c => !CAPABILITIES.includes(c)) ||
20
+ new Set(adapter.capabilities).size !== adapter.capabilities.length ||
21
+ typeof adapter.validate !== 'function' || typeof adapter.plan !== 'function') fail('provider.interface');
22
+ for (const c of requiredCapabilities(declaration))
23
+ if (!adapter.capabilities.includes(c)) fail('provider.unsupported');
24
+ return adapter;
25
+ }
@@ -0,0 +1,26 @@
1
+ import { fail, MAX_INPUT_BYTES } from '../contracts/parse.js';
2
+ import { sourceText } from './source.js';
3
+ import { skills, agents, mcp } from './native.js';
4
+
5
+ // Direct, workspace-local delivery. No plugin installation, mutable cache,
6
+ // account settings, trust grants or harness invocation.
7
+ const capabilities = Object.freeze(['skills', 'agents', 'mcp', 'entry-instructions']);
8
+ function render(context) {
9
+ const declaration = context.pipeline.providers.kimi;
10
+ if (!declaration || declaration.requires.some(value => !capabilities.includes(value))) fail('provider.unsupported');
11
+ const requests = [];
12
+ if (declaration.skills !== null) requests.push(...skills(context, 'kimi', declaration.skills));
13
+ if (declaration.agents !== null) requests.push(...agents(context, 'kimi', declaration.agents).requests);
14
+ if (declaration.mcp !== null) requests.push({ owner: 'kimi', path: '.kimi-code/mcp.json',
15
+ kind: 'json-fields', fields: mcp(context, 'kimi', declaration.mcp) });
16
+ // Shared adapter owns AGENTS.md and its provider entry link.
17
+ if (declaration.entryInstructions !== null) {
18
+ const value = sourceText(context, declaration.entryInstructions);
19
+ if (!value.trim() || value.includes('\0') || Buffer.byteLength(value) > MAX_INPUT_BYTES) fail('provider.format');
20
+ }
21
+ return requests;
22
+ }
23
+ export const kimiAdapter = Object.freeze({ id: 'kimi', version: '1', capabilities,
24
+ async validate(context) { render(context); return { valid: true, runtime: 'not-run' }; },
25
+ async plan(context) { return render(context); },
26
+ });