@awebai/oats 0.25.9 → 0.26.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 (177) hide show
  1. package/README.md +8 -6
  2. package/bin/oats.mjs +576 -1714
  3. package/capabilities/oats-authoring/oats-package.json +2 -2
  4. package/capabilities/oats-authoring/oats.json +2 -2
  5. package/capabilities/oats-authoring/skills/integration-authoring/SKILL.md +46 -25
  6. package/capabilities/oats-authoring/skills/soul-craft/SKILL.md +13 -6
  7. package/capabilities/oats-aweb/bin/oats-aweb.mjs +279 -93
  8. package/capabilities/oats-aweb/injects/aweb.md +7 -2
  9. package/capabilities/oats-aweb/lib/binding-wire.mjs +89 -13
  10. package/capabilities/oats-aweb/lib/captured-native.mjs +1 -1
  11. package/capabilities/oats-aweb/lib/grant-custody.mjs +38 -0
  12. package/capabilities/oats-aweb/oats.json +8 -4
  13. package/capabilities/oats-jira/bin/oats-jira.mjs +4 -4
  14. package/capabilities/oats-jira/oats.json +2 -2
  15. package/capabilities/oats-jira/skills/jira-tasks/SKILL.md +6 -3
  16. package/capabilities/oats-linear/bin/oats-linear-hook.mjs +6 -4
  17. package/capabilities/oats-linear/oats.json +2 -2
  18. package/capabilities/oats-linear/skills/linear-tasks/SKILL.md +6 -0
  19. package/capabilities/oats-review/oats.json +3 -2
  20. package/docs/capabilities.md +218 -47
  21. package/docs/capability-manifest.schema.json +13 -4
  22. package/docs/configuration.md +17 -5
  23. package/docs/conventions.md +16 -26
  24. package/docs/design/2026-09-08-expert-assisted-deployment-proposal.md +1 -1
  25. package/docs/design/2026-09-13-knowledge-and-memory-direction.md +3 -3
  26. package/docs/design/2026-09-14-portable-souls-and-git-workspaces.md +2 -2
  27. package/docs/design/2026-09-15-portable-souls-handoff.md +2 -2
  28. package/docs/design/2026-09-15-portable-souls-implementation.md +1 -1
  29. package/docs/design/2026-09-20-redesign-program-board.md +2 -2
  30. package/docs/design/2026-09-23-workspace-module-contracts.md +1 -1
  31. package/docs/design/2026-09-23-workspace-v2-implementation-plan.md +1 -1
  32. package/docs/design/2026-09-24-desktop-phase-f-boundary.md +34 -1
  33. package/docs/design/2026-09-24-phase-d-plan.md +57 -0
  34. package/docs/design/2026-09-25-teams-contract.md +226 -0
  35. package/docs/design/README.md +3 -3
  36. package/docs/design/launch-configurations.md +20 -16
  37. package/docs/design/operations-contract.md +27 -10
  38. package/docs/desktop-cli-api.md +537 -261
  39. package/docs/desktop-instance-start.md +1 -1
  40. package/docs/desktop.md +7 -13
  41. package/docs/execution-targets.md +16 -18
  42. package/docs/first-team.md +14 -17
  43. package/docs/implementation.md +28 -59
  44. package/docs/integrations.md +64 -33
  45. package/docs/knowledge-capability-authoring.md +1 -1
  46. package/docs/knowledge-reference/package-craft.md +10 -8
  47. package/docs/knowledge-theory.md +1 -1
  48. package/docs/knowledge.md +10 -11
  49. package/docs/layers.md +16 -17
  50. package/docs/oats-local.schema.json +29 -1
  51. package/docs/oats-membership.schema.json +5 -3
  52. package/docs/oats-package.schema.json +2 -2
  53. package/docs/oats-workspace.schema.json +1 -1
  54. package/docs/{official-marketplace.md → official-catalog.md} +15 -16
  55. package/docs/packages.md +75 -52
  56. package/docs/release-notes/v0.22.0.md +1 -1
  57. package/docs/release-notes/v0.23.1.md +1 -1
  58. package/docs/release-notes/v0.26.0.md +670 -0
  59. package/docs/schedules.md +48 -126
  60. package/docs/soul.schema.json +11 -4
  61. package/docs/souls-and-instances.md +56 -43
  62. package/docs/workspaces.md +80 -58
  63. package/injects/instance-boundary.md +1 -1
  64. package/injects/work-attached.md +1 -1
  65. package/injects/work-workspace.md +2 -2
  66. package/lib/{portable-files.mjs → bounded-read.mjs} +6 -6
  67. package/lib/{portable-values.mjs → canonical-json.mjs} +3 -12
  68. package/lib/capability-contract.mjs +110 -0
  69. package/lib/config-data.mjs +2 -2
  70. package/lib/core.mjs +700 -4824
  71. package/lib/digest.mjs +12 -0
  72. package/lib/instance-inspect.mjs +396 -0
  73. package/lib/instance-lifecycle.mjs +3 -4
  74. package/lib/instance-resolution.mjs +212 -26
  75. package/lib/instruction-composition.mjs +0 -20
  76. package/lib/materialize.mjs +6 -4
  77. package/lib/operator-dispatch.mjs +33 -13
  78. package/lib/packages.mjs +25 -190
  79. package/lib/provider-binding.mjs +4 -2
  80. package/lib/provider-reasons.mjs +3 -68
  81. package/lib/resolve.mjs +204 -68
  82. package/lib/schedule.mjs +97 -272
  83. package/lib/servers.mjs +13 -13
  84. package/lib/{portable-shape.mjs → shape.mjs} +4 -3
  85. package/lib/tree-copy.mjs +44 -0
  86. package/lib/workspace.mjs +125 -20
  87. package/package-catalog.json +6 -6
  88. package/package.json +1 -1
  89. package/skills/integration-authoring/SKILL.md +48 -40
  90. package/skills/oats-getting-started/SKILL.md +105 -110
  91. package/skills/oats-support/SKILL.md +2 -2
  92. package/skills/soul-craft/SKILL.md +13 -6
  93. package/bin/oats-pi-sdk-host.mjs +0 -17
  94. package/docs/2026-09-03-architecture-proposal.md +0 -642
  95. package/docs/artifact-approvals.schema.json +0 -7
  96. package/docs/captured-invocation-context.schema.json +0 -7
  97. package/docs/captured-resolution.schema.json +0 -7
  98. package/docs/design/package-engine-contract.md +0 -813
  99. package/docs/design/package-runtime-api.md +0 -588
  100. package/docs/desktop-succession.md +0 -57
  101. package/docs/execution-capsule.schema.json +0 -108
  102. package/docs/first-team-demo.md +0 -92
  103. package/docs/knowledge-migration.md +0 -147
  104. package/docs/migration-from-oas.md +0 -103
  105. package/docs/oats-config.schema.json +0 -172
  106. package/docs/oats-lock-v3.schema.json +0 -7
  107. package/docs/oats-lock.schema.json +0 -175
  108. package/docs/operating-team-migration.md +0 -470
  109. package/docs/portable.schema.json +0 -2512
  110. package/docs/provider-check-input.schema.json +0 -7
  111. package/docs/rebuild-to-v2.md +0 -511
  112. package/docs/workspace-adoption.md +0 -74
  113. package/injects/framework-workspace.md +0 -7
  114. package/injects/local-soul.md +0 -19
  115. package/injects/oats-portable.md +0 -20
  116. package/injects/oats.md +0 -11
  117. package/injects/portable-instance-boundary.md +0 -39
  118. package/injects/portable-work-directory.md +0 -29
  119. package/lib/artifact-approvals.mjs +0 -120
  120. package/lib/artifact-tree.mjs +0 -141
  121. package/lib/capability-artifacts.mjs +0 -179
  122. package/lib/capability-execution.mjs +0 -15
  123. package/lib/capability-inputs.mjs +0 -39
  124. package/lib/capability-provenance.mjs +0 -231
  125. package/lib/captured-action-shape.mjs +0 -21
  126. package/lib/captured-admission-shape.mjs +0 -20
  127. package/lib/captured-binding-file.mjs +0 -36
  128. package/lib/captured-dispatch.mjs +0 -66
  129. package/lib/captured-instance-index.mjs +0 -277
  130. package/lib/captured-invocation-context.mjs +0 -130
  131. package/lib/captured-launch-request.mjs +0 -66
  132. package/lib/captured-operation-process.mjs +0 -15
  133. package/lib/captured-pi-custody.mjs +0 -29
  134. package/lib/captured-pi-host.mjs +0 -167
  135. package/lib/captured-pi-outcome.mjs +0 -172
  136. package/lib/captured-resolutions.mjs +0 -275
  137. package/lib/captured-scaffold.mjs +0 -87
  138. package/lib/captured-selector.mjs +0 -28
  139. package/lib/captured-session-backend.mjs +0 -52
  140. package/lib/captured-source-receipt-file.mjs +0 -72
  141. package/lib/helper-injection-policy.mjs +0 -104
  142. package/lib/legacy-lock-codec.mjs +0 -106
  143. package/lib/manifest-settings.mjs +0 -84
  144. package/lib/package-closure.mjs +0 -48
  145. package/lib/package-materialization.mjs +0 -83
  146. package/lib/pi-sdk-host.mjs +0 -229
  147. package/lib/portable-artifacts.mjs +0 -115
  148. package/lib/portable-choices.mjs +0 -82
  149. package/lib/portable-composition.mjs +0 -136
  150. package/lib/portable-digest.mjs +0 -105
  151. package/lib/portable-identity.mjs +0 -40
  152. package/lib/portable-lock.mjs +0 -117
  153. package/lib/portable-onboarding-request.mjs +0 -49
  154. package/lib/portable-onboarding.mjs +0 -256
  155. package/lib/portable-package-preparation.mjs +0 -188
  156. package/lib/portable-policy.mjs +0 -44
  157. package/lib/portable-soul.mjs +0 -42
  158. package/lib/portable-state.mjs +0 -80
  159. package/lib/prepare-composition.mjs +0 -170
  160. package/lib/prepared-bindings.mjs +0 -92
  161. package/lib/prepared-resources.mjs +0 -127
  162. package/lib/provider-binding-broker.mjs +0 -65
  163. package/lib/provider-binding-wire.mjs +0 -116
  164. package/lib/readiness.mjs +0 -225
  165. package/lib/repository-observation.mjs +0 -226
  166. package/lib/resolution-shape.mjs +0 -393
  167. package/lib/schedule-capsule.mjs +0 -206
  168. package/lib/soul-constraints.mjs +0 -40
  169. package/lib/source-projection.mjs +0 -84
  170. package/lib/source-spec.mjs +0 -189
  171. package/lib/workspace-definition.mjs +0 -126
  172. package/lib/workspace-discovery.mjs +0 -146
  173. package/skills/oats/SKILL.md +0 -162
  174. package/skills/oats-config/SKILL.md +0 -164
  175. package/skills/oats-packages/SKILL.md +0 -184
  176. package/skills/oats-portable/SKILL.md +0 -115
  177. package/skills/oats-portable-artifacts/SKILL.md +0 -63
@@ -1,256 +0,0 @@
1
- /** Read-only fresh setup and source discovery facade.
2
- *
3
- * Existing project content is allowed. Relevant OATS-managed state produces a
4
- * separate-path hold; nothing is deleted, rewritten, installed, enrolled or
5
- * prepared here. Repository reads are delegated to the existing transaction and
6
- * workspace discovery/parser stack. */
7
- import { lstatSync, opendirSync, realpathSync } from "node:fs";
8
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
9
- import { createWorkspaceDiscovery } from "./workspace-discovery.mjs";
10
- import { parseMemberExports } from "./workspace-definition.mjs";
11
- import { canonicalJson, compareUtf8, freezeJson } from "./portable-values.mjs";
12
- import { validateOrigin } from "./resolution-shape.mjs";
13
- import { oatsError } from "./errors.mjs";
14
-
15
- export const PORTABLE_ONBOARDING_VERSION = 1;
16
- const PREPARATION_ONLY_FIELDS = Object.freeze(["operator", "launch", "helperLaunches", "mode", "allowLocalPaths"]);
17
- const issued = new WeakMap();
18
- const preflightWitnesses = new WeakMap();
19
- const MANAGED_PATHS = Object.freeze([
20
- ["deployment-config", "oats-config.yaml"],
21
- ["selection-lock", "oats-lock.json"],
22
- ["schedules", "oats-schedules.json"],
23
- ["portable-state", ".agents/portable"],
24
- ["captured-resolutions", ".agents/resolutions"],
25
- ["migration-evidence", ".agents/resolution-evidence"],
26
- ["installed-capabilities", ".agents/capabilities/installed"],
27
- ["retained-capabilities", ".agents/capabilities/artifacts"],
28
- ["retained-souls", ".agents/soul-artifacts"],
29
- ["retained-resources", ".agents/resource-artifacts"],
30
- ["schedule-state", ".agents/schedules"],
31
- ["installed-packages", ".agents/packages/installed"],
32
- ["native-history", ".oats-native-record"],
33
- ]);
34
-
35
- const present = (path) => { try { return lstatSync(path); } catch (error) { if (error.code === "ENOENT") return null; throw error; } };
36
- function exact(value, allowed, required, where) {
37
- if (!value || typeof value !== "object" || Array.isArray(value)) throw oatsError("invalid-declaration", `${where} must be an object`);
38
- for (const key of Object.keys(value)) if (!allowed.includes(key)) throw oatsError("invalid-declaration", `${where} has unsupported field ${key}`);
39
- for (const key of required) if (!Object.hasOwn(value, key)) throw oatsError("invalid-declaration", `${where} is missing ${key}`);
40
- return value;
41
- }
42
- function canonicalExistingDirectory(path, label) {
43
- if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path) throw oatsError("invalid-declaration", `${label} must be a normalized absolute path`);
44
- let actual;
45
- try { actual = realpathSync(path); } catch (error) { if (error.code === "ENOENT") throw oatsError("source-unavailable", `${label} is absent`); throw error; }
46
- if (actual !== path || !lstatSync(actual).isDirectory()) throw oatsError("invalid-declaration", `${label} must be a real non-symlinked directory`);
47
- return actual;
48
- }
49
- function directoryIdentity(path) {
50
- const stat = lstatSync(path);
51
- if (!stat.isDirectory()) throw oatsError("selection-changed", "fresh setup directory is no longer a real directory");
52
- return { dev: stat.dev, ino: stat.ino };
53
- }
54
- function freshDeploymentPath(path) {
55
- if (typeof path !== "string" || !isAbsolute(path) || resolve(path) !== path) throw oatsError("invalid-declaration", "fresh deployment must be a normalized absolute path");
56
- const stat = present(path);
57
- if (stat) {
58
- if (!stat.isDirectory() || realpathSync(path) !== path) throw oatsError("invalid-declaration", "fresh deployment path must be a real non-symlinked directory");
59
- return { path, state: "existing", identity: { dev: stat.dev, ino: stat.ino } };
60
- }
61
- let parent;
62
- try { parent = realpathSync(dirname(path)); }
63
- catch (error) { if (error.code === "ENOENT") throw oatsError("source-unavailable", "fresh deployment parent is absent"); throw error; }
64
- if (parent !== dirname(path) || !lstatSync(parent).isDirectory()) throw oatsError("invalid-declaration", "fresh deployment parent must be a real non-symlinked directory");
65
- return { path, state: "absent", parent: directoryIdentity(parent) };
66
- }
67
- function relativePath(root, path) { return relative(root, path).split(sep).join("/"); }
68
- function scanInstanceState(root, base, budget) {
69
- const found = [], path = join(root, base), stat = present(path);
70
- if (!stat) return found;
71
- if (!stat.isDirectory() || realpathSync(path) !== path) return [{ kind: "managed-agents-root", path: base }];
72
- const directory = opendirSync(path);
73
- try {
74
- for (;;) {
75
- const entry = directory.readSync();
76
- if (entry === null) break;
77
- if (++budget.visited > budget.maxEntries) throw oatsError("resource-limit", "fresh deployment instance scan entry limit exceeded");
78
- if (!entry.isDirectory()) continue;
79
- for (const [kind, child] of [["instance-homes", "instances"], ["retirement-state", ".oats-retirement"]]) {
80
- const target = join(path, entry.name, child);
81
- if (present(target)) found.push({ kind, path: relativePath(root, target) });
82
- }
83
- }
84
- } finally { directory.closeSync(); }
85
- return found;
86
- }
87
-
88
- /** Detect only known deployment-owned state. Ordinary files, Git metadata,
89
- * authored souls/capabilities and source repositories do not make a path dirty. */
90
- export function preflightFreshDeployment({ deployment, maxEntries = 4096 }) {
91
- canonicalJson({ deployment, maxEntries });
92
- if (!Number.isSafeInteger(maxEntries) || maxEntries < 1 || maxEntries > 100_000) throw oatsError("resource-limit", "invalid fresh deployment scan budget");
93
- const selected = freshDeploymentPath(deployment), managedState = [];
94
- if (selected.state === "existing") {
95
- for (const [kind, relative] of MANAGED_PATHS) if (present(join(selected.path, relative))) managedState.push({ kind, path: relative });
96
- const budget = { visited: 0, maxEntries };
97
- managedState.push(...scanInstanceState(selected.path, "agents", budget), ...scanInstanceState(selected.path, "local-agents", budget));
98
- }
99
- managedState.sort((a, b) => compareUtf8(`${a.path}:${a.kind}`, `${b.path}:${b.kind}`));
100
- if (canonicalJson(freshDeploymentPath(deployment)) !== canonicalJson(selected)) {
101
- throw oatsError("selection-changed", "fresh deployment changed during preflight; inspect it again");
102
- }
103
- const status = managedState.length ? "separate-deployment-required" : "ready";
104
- const result = freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, status,
105
- deployment: { path: selected.path, state: selected.state }, managedState,
106
- advice: managedState.length ? ["Preserve this deployment and choose a separate fresh deployment path; no existing state was changed."] : [],
107
- effects: { writes: false, deletes: false, installs: false, enrollment: false } });
108
- preflightWitnesses.set(result, selected);
109
- return result;
110
- }
111
-
112
- export function inspectWorkTarget(path) {
113
- const canonical = canonicalExistingDirectory(path, "work target"), marker = present(join(canonical, ".git"));
114
- return { path: canonical, state: "existing", git: marker ? { present: true, kind: marker.isDirectory() ? "directory" : marker.isFile() ? "file" : "other" } : { present: false, kind: null } };
115
- }
116
- function sourceOrigin(workspace, alias) {
117
- const index = workspace?.parsed.imports.findIndex((entry) => entry.alias === alias) ?? -1;
118
- if (index < 0) throw oatsError("export-not-found", "requested alias is not advertised by the explicit workspace");
119
- return { reference: workspace.parsed.imports[index], origin: { ...workspace.parsed.origins[`/imports/${index}`], kind: "import-adoption" } };
120
- }
121
- function explicitContext(input) {
122
- const hasWorkspace = Object.hasOwn(input, "workspace");
123
- const hasStandalone = Object.hasOwn(input, "standaloneContextKey");
124
- if (hasWorkspace && hasStandalone) throw oatsError("invalid-declaration", "workspace and standaloneContextKey are mutually exclusive contexts");
125
- if (hasWorkspace) return null;
126
- if (!hasStandalone) throw oatsError("needs-configuration", "standalone onboarding requires an explicit standaloneContextKey (null explicitly means no private messaging context)");
127
- const key = input.standaloneContextKey;
128
- if (key !== null && (typeof key !== "string" || !key || Buffer.byteLength(key, "utf8") > 256 || key.trim() !== key || /[\x00-\x1f\x7f]/.test(key))) {
129
- throw oatsError("invalid-declaration", "standaloneContextKey must be explicit null or non-empty text up to 256 UTF-8 bytes");
130
- }
131
- return { kind: "standalone", key };
132
- }
133
- function catalogSelection(value, count) {
134
- if (value === undefined) return [];
135
- if (!Array.isArray(value) || value.length > count || value.some((index) => !Number.isSafeInteger(index) || index < 0 || index >= count)
136
- || new Set(value).size !== value.length) throw oatsError("invalid-declaration", "catalogIndexes must be unique declared workspace catalog indexes");
137
- return [...value].sort((a, b) => a - b);
138
- }
139
-
140
- /** Inspect one explicit source and optional explicit workspace/member/catalogs.
141
- * The caller owns the repository transaction lifetime. */
142
- export function inspectPortableOnboarding(input, { repositories } = {}) {
143
- canonicalJson(input);
144
- exact(input, ["deployment", "workTarget", "source", "origin", "workspace", "member", "catalogIndexes", "standaloneContextKey", ...PREPARATION_ONLY_FIELDS],
145
- ["deployment", "workTarget", "source", "origin"], "portable onboarding inspection");
146
- if (!repositories || typeof repositories !== "object") throw oatsError("invalid-source", "portable onboarding needs an explicit repository transaction");
147
- validateOrigin(input.origin);
148
- const standalone = explicitContext(input);
149
- const deployment = preflightFreshDeployment({ deployment: input.deployment });
150
- const target = inspectWorkTarget(input.workTarget), discovery = createWorkspaceDiscovery(repositories);
151
- const ignored = PREPARATION_ONLY_FIELDS.filter(field => Object.hasOwn(input, field));
152
- // Retain authored data privately for explicit request export ONLY. Inspection
153
- // neither validates provider/launch semantics nor exposes these values.
154
- const witness = { deployment: preflightWitnesses.get(deployment), work: directoryIdentity(target.path),
155
- preparationFields: Object.fromEntries(ignored.map(field => [field, structuredClone(input[field])])) };
156
- const workspace = Object.hasOwn(input, "workspace") ? discovery.readWorkspace(input.workspace) : null;
157
- const selected = typeof input.source === "string" ? sourceOrigin(workspace, input.source) : { reference: input.source, origin: input.origin };
158
- const imported = discovery.importSoul(selected.reference, { origin: selected.origin });
159
- const indexDocument = repositories.readFile(imported.observation, "oats.yaml");
160
- const exports = parseMemberExports(indexDocument.bytes, { origin: indexDocument.origin });
161
- const membership = input.member ? (() => {
162
- if (!workspace) throw oatsError("needs-configuration", "member inspection needs an explicit workspace");
163
- return discovery.checkMember(workspace, input.member);
164
- })() : { schemaVersion: 1, status: "not-requested", evidence: [], problems: [] };
165
- const catalogs = [];
166
- const selectedCatalogs = catalogSelection(input.catalogIndexes, workspace?.parsed.catalogs.length ?? 0);
167
- if (selectedCatalogs.length && !workspace) throw oatsError("needs-configuration", "catalog inspection needs an explicit workspace");
168
- for (const index of selectedCatalogs) {
169
- const declared = workspace.parsed.catalogs[index], base = workspace.parsed.origins[`/catalogs/${index}`];
170
- if (!declared.path) throw oatsError("needs-configuration", "catalog inspection needs an explicit descriptor path");
171
- const origin = { ...base, kind: "workspace-default" }; validateOrigin(origin);
172
- const observation = repositories.observe(declared.source, { ...(declared.revision ? { revision: declared.revision } : {}), origin });
173
- const document = repositories.readFile(observation, declared.path);
174
- catalogs.push({ index, declaration: declared, source: observation.source, document: document.origin });
175
- }
176
- const declaredTeams = workspace?.parsed.declaration.teams ?? {};
177
- const status = deployment.status !== "ready" ? deployment.status
178
- : input.member && membership.status !== "eligible" ? "needs-configuration" : "ready-for-preparation";
179
- const result = freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, status, deployment, workTarget: target, ignored,
180
- source: { location: imported.reference.source, reference: imported.reference, identity: imported.identity,
181
- revision: imported.observation.source, alias: imported.reference.alias, exportPath: imported.reference.soul,
182
- definition: imported.definition, roots: imported.roots, exports: exports.exports, provenance: imported.provenance },
183
- workspace: workspace ? { request: structuredClone(input.workspace), identity: workspace.identity, source: workspace.source,
184
- name: workspace.parsed.declaration.name, members: workspace.parsed.members,
185
- imports: workspace.parsed.imports, catalogs: workspace.parsed.catalogs } : null,
186
- context: workspace ? { kind: "workspace", identity: workspace.identity } : standalone,
187
- repositoryMembership: { request: input.member ? structuredClone(input.member) : null, result: membership },
188
- teams: { declared: declaredTeams, enrollment: "not-performed", privateTeamQualification: "not-evaluated" },
189
- catalogs, problems: membership.problems ?? [],
190
- effects: { deploymentWrites: false, repositoryReads: true, repositoryScratch: "caller-owned",
191
- installs: false, activation: false, credentials: false, teams: false, jobs: false } });
192
- issued.set(result, witness);
193
- if (status === "ready-for-preparation") recheckFreshOnboarding(result);
194
- return result;
195
- }
196
-
197
- /** Metadata-only public view, NOT a serialized fresh-inspection witness or a
198
- * preparation request. Provider payloads/adoption values have not gone through
199
- * their owner's nonsecret classification and must not leak through inspection. */
200
- export function describePortableOnboarding(inspection) {
201
- if (!issued.has(inspection)) throw oatsError("invalid-declaration", "inspection summary requires an issued onboarding view");
202
- const reference = value => ({ source: value.source, soul: value.soul, revision: value.revision, alias: value.alias,
203
- adoptionPresent: Object.hasOwn(value, "adoption") });
204
- const { reference: selected, exports, ...source } = inspection.source;
205
- return freezeJson({ ...inspection,
206
- source: { ...source, adoptionPresent: Object.hasOwn(selected, "adoption"), exports: {
207
- souls: (exports.souls ?? []).map(({ path, definition, description }) => ({ path, definition, ...(description === undefined ? {} : { description }) })),
208
- packages: (exports.packages ?? []).map(({ path, description }) => ({ path, ...(description === undefined ? {} : { description }) })),
209
- knowledge: (exports.knowledge ?? []).map(({ contract, version }) => ({ contract, version, payloadOmitted: true })),
210
- } },
211
- workspace: inspection.workspace ? { ...inspection.workspace, imports: inspection.workspace.imports.map(reference) } : null,
212
- omitted: { providerPayloads: true, adoptionValues: true },
213
- });
214
- }
215
-
216
- /** Ephemeral local root custody, not a new persistent authority/schema. Ordinary
217
- * project content changes are allowed; replacing the inspected directories or
218
- * provisioning a previously absent deployment requires a fresh inspection. */
219
- export function recheckFreshOnboarding(inspection) {
220
- const witness = issued.get(inspection);
221
- if (!witness) throw oatsError("invalid-declaration", "fresh target recheck requires an issued inspection");
222
- const current = preflightFreshDeployment({ deployment: inspection.deployment.deployment.path });
223
- if (current.status !== "ready") throw oatsError("fresh-deployment-required", "fresh deployment acquired managed state after inspection; preserve it and choose another path");
224
- if (canonicalJson(preflightWitnesses.get(current)) !== canonicalJson(witness.deployment)) {
225
- throw oatsError("selection-changed", "inspected deployment or its absent-path parent changed; inspect it again");
226
- }
227
- let work;
228
- try { work = directoryIdentity(canonicalExistingDirectory(inspection.workTarget.path, "work target")); }
229
- catch { throw oatsError("selection-changed", "inspected work target is no longer available at its owned path"); }
230
- if (canonicalJson(work) !== canonicalJson(witness.work)) throw oatsError("selection-changed", "inspected work target directory changed; inspect it again");
231
- return current;
232
- }
233
-
234
- /** Build data for the existing preparation API. This function never invokes it.
235
- * Work target remains adjacent and distinct from deployment/source inputs. */
236
- export function buildFreshPreparationRequest(inspection, options = {}) {
237
- canonicalJson(options);
238
- exact(options, ["operator", "mode", "allowLocalPaths"], [], "fresh preparation options");
239
- if (!issued.has(inspection)) throw oatsError("invalid-declaration", "fresh preparation requires an inspection issued by this module");
240
- if (inspection.status !== "ready-for-preparation") throw oatsError("fresh-deployment-required", "fresh preparation is held by deployment or membership preflight");
241
- const { operator, mode, allowLocalPaths = false } = options;
242
- if (typeof allowLocalPaths !== "boolean") throw oatsError("invalid-declaration", "allowLocalPaths must be boolean");
243
- if (mode !== undefined && (typeof mode !== "string" || !mode.trim())) throw oatsError("invalid-declaration", "preparation mode must be non-empty text");
244
- const input = { deployment: inspection.deployment.deployment.path, workTarget: inspection.workTarget.path,
245
- source: inspection.source.reference,
246
- origin: inspection.source.revision.provenance[0], allowLocalPaths,
247
- ...(inspection.workspace ? { workspace: inspection.workspace.request } : { standaloneContextKey: inspection.context.key }),
248
- ...(inspection.repositoryMembership.request ? { member: inspection.repositoryMembership.request } : {}),
249
- ...issued.get(inspection).preparationFields,
250
- ...(operator === undefined ? {} : { operator }), ...(mode === undefined ? {} : { mode }),
251
- ...(Object.hasOwn(options, "allowLocalPaths") ? { allowLocalPaths } : {}) };
252
- canonicalJson(input);
253
- return freezeJson({ schemaVersion: PORTABLE_ONBOARDING_VERSION, operation: "prepare", persisted: false,
254
- preparation: input, workTarget: inspection.workTarget,
255
- effects: { writes: false, installs: false, activation: false, enrollment: false } });
256
- }
@@ -1,188 +0,0 @@
1
- /** Package-side portable preparation over frozen observations. No current lock,
2
- * config cascade, approval or activation lookup; caller owns final composition/CAS. */
3
- import { chmodSync, lstatSync, mkdtempSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
4
- import { isAbsolute, join, posix, relative } from "node:path";
5
- import { canonicalJson, compareUtf8, decodeUtf8, parseStrictJson } from "./portable-values.mjs";
6
- import { PACKAGE_FORMAT, bytesIntegrity, treeIntegrity } from "./portable-digest.mjs";
7
- import { readPortableBytes } from "./portable-files.mjs";
8
- import { removeOwnedStaging } from "./artifact-tree.mjs";
9
- import { createSourceProjection, validateProjectionPath } from "./source-projection.mjs";
10
- import { resolvePackageClosure } from "./package-closure.mjs";
11
- import { parseLockedSource3, parsePackageDependency3, parseRepositorySource, portablePath, validateRepoAnchor, validateSelectionSource } from "./source-spec.mjs";
12
- import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
13
- import { objectAt } from "./portable-shape.mjs";
14
- import { validateArtifactSet, validateOrigin } from "./resolution-shape.mjs";
15
- import { retainPortableArtifact } from "./portable-artifacts.mjs";
16
- import { oatsError } from "./errors.mjs";
17
-
18
- const outside = (root, target) => { const path = relative(root, target); return path === ".." || path.startsWith("../") || path.startsWith("..\\") || isAbsolute(path); };
19
- function copyLocalPayload(source, destination) {
20
- const root = realpathSync(source), parent = realpathSync(join(destination, "..")), rootStat = lstatSync(root);
21
- if (!rootStat.isDirectory() || !outside(root, parent)) throw oatsError("invalid-source", "package input must be a directory outside its staging tree");
22
- const entries = []; let count = 0, bytes = 0;
23
- const inspect = (directory, parts) => {
24
- if (parts.length > 64) throw oatsError("resource-limit", "local package source depth exceeded");
25
- for (const raw of readdirSync(directory, { encoding: "buffer" })) {
26
- if (++count > 10_000) throw oatsError("resource-limit", "local package source entry budget exceeded");
27
- const name = decodeUtf8(raw), lowered = name.toLowerCase();
28
- if (lowered === ".git") continue; // Never read/copy Git administrative state, at any depth.
29
- if ([".aw", ".agents", ".oats-state"].includes(lowered) || (lowered === "instances" && parts.at(-2)?.toLowerCase() === "agents")) {
30
- throw oatsError("source-incomplete", "local package subtree contains private deployment/auth/instance state");
31
- }
32
- const path = [...parts, name].join("/"), file = join(directory, name), stat = lstatSync(file);
33
- validateProjectionPath(path);
34
- if (!stat.isDirectory() && !stat.isFile() && !stat.isSymbolicLink()) throw oatsError("invalid-source", "local package source contains a special file");
35
- if (stat.isFile() && (bytes += stat.size) > 256 * 1024 * 1024) throw oatsError("resource-limit", "local package source byte budget exceeded");
36
- entries.push({ path, file, stat });
37
- if (stat.isDirectory()) inspect(file, [...parts, name]);
38
- }
39
- };
40
- inspect(root, []); // Complete no-follow privacy/size preflight before any copy.
41
- const projection = createSourceProjection(destination);
42
- const unchanged = (entry) => {
43
- const actual = lstatSync(entry.file), before = entry.stat;
44
- if (actual.dev !== before.dev || actual.ino !== before.ino || actual.mode !== before.mode || actual.size !== before.size
45
- || actual.mtimeMs !== before.mtimeMs || actual.ctimeMs !== before.ctimeMs) throw oatsError("integrity-drift", "local package source changed during snapshot");
46
- };
47
- for (const entry of entries) {
48
- unchanged(entry);
49
- if (entry.stat.isDirectory()) projection.directory(entry.path, entry.stat.mode & 0o7777);
50
- else if (entry.stat.isSymbolicLink()) projection.link(entry.path, readlinkSync(entry.file));
51
- else projection.copyFile(entry.path, entry.file, entry.stat.mode & 0o7777);
52
- unchanged(entry);
53
- }
54
- projection.finish(); chmodSync(destination, rootStat.mode & 0o7777);
55
- }
56
-
57
- export function preparePackageArtifacts({ requests, deployment, directory, repositories, kernel, observations = [], catalog, allowLocalPaths = false }) {
58
- canonicalJson(requests);
59
- if (!Array.isArray(requests) || !requests.length || typeof directory !== "string" || !isAbsolute(directory)
60
- || typeof deployment !== "string" || !isAbsolute(deployment) || typeof allowLocalPaths !== "boolean") throw oatsError("invalid-source", "package preparation needs explicit requests, scope and scratch directory");
61
- for (const name of ["loadPackageManifestAt", "capabilityCompatibility", "assertPlatformInvariantLocks", "materializeCapability"]) {
62
- if (typeof kernel?.[name] !== "function") throw new TypeError(`package preparation requires kernel.${name}`);
63
- }
64
- for (const request of requests) {
65
- objectAt(request, ["source", "origin", "capability"], ["source", "origin"]); validateOrigin(request.origin);
66
- if (request.capability !== undefined && !isMaterializedCapabilityId(request.capability)) throw oatsError("invalid-source", "invalid requested capability identity");
67
- }
68
- const staging = mkdtempSync(join(realpathSync(directory), ".package-preparation-")), owned = lstatSync(staging);
69
- const cached = new Map(), sourceObservations = [...observations]; let counter = 0, closed = false;
70
- const cleanup = () => {
71
- if (closed) return;
72
- const current = lstatSync(staging);
73
- if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("source-incomplete", "package staging cleanup lost ownership");
74
- removeOwnedStaging(staging); closed = true;
75
- };
76
- const observe = (source, selector, origin) => {
77
- const existing = sourceObservations.find((value) => value.source.remote === source && (value.source.commit === selector || value.source.selector === selector));
78
- if (existing) return existing;
79
- const observation = repositories.observe(source, { ...(selector === undefined ? {} : { revision: selector }), origin });
80
- sourceObservations.push(observation);
81
- return observation;
82
- };
83
- const readPackage = (request) => {
84
- validateOrigin(request.origin);
85
- let selected = request.source;
86
- if (selected?.kind === "catalog") {
87
- const normalized = parsePackageDependency3(selected.source);
88
- if (canonicalJson(normalized) !== canonicalJson(selected)) throw oatsError("invalid-source", "catalog request fields disagree");
89
- } else validateSelectionSource(selected);
90
- const key = canonicalJson(selected);
91
- if (cached.has(key)) return cached.get(key);
92
- let originalSource, packagePath, commit, payload, sourceDocument, repoAnchor = null, localBase;
93
- const destination = join(staging, `package-${counter++}`);
94
- let repository, selector;
95
- if (selected.kind === "repo") {
96
- repoAnchor = selected.anchor;
97
- if (repoAnchor.source.startsWith("path:")) {
98
- const root = parseLockedSource3(repoAnchor.source, ".").localPath;
99
- const localPath = join(root, selected.path);
100
- selected = { kind: "path", source: `path:${localPath}`, path: ".", localPath, portable: false };
101
- } else {
102
- repository = repoAnchor.source; selector = repoAnchor.revision; packagePath = selected.path;
103
- originalSource = `${repository}@${selector}`;
104
- }
105
- }
106
- if (selected.kind === "catalog") {
107
- if (typeof catalog !== "function") throw oatsError("needs-configuration", "package dependency requires an explicit catalog resolver");
108
- const entry = catalog(selected.id, selected.selector);
109
- if (!entry || typeof entry.url !== "string") throw oatsError("invalid-source", "catalog did not resolve a repository");
110
- repository = parseRepositorySource(`git:${entry.url}`).normalized;
111
- selector = entry.ref; packagePath = entry.path ?? "oats-package"; originalSource = selected.source;
112
- } else if (selected.kind === "git") {
113
- repository = parseRepositorySource(`git:${selected.url}`).normalized;
114
- selector = selected.selector; packagePath = selected.path; originalSource = selected.source;
115
- }
116
- if (selected.kind === "path") {
117
- if (!allowLocalPaths) throw oatsError("needs-configuration", "local package input requires explicit authorization");
118
- localBase = selected.localPath; originalSource = selected.source; packagePath = "."; commit = "local";
119
- copyLocalPayload(localBase, destination); payload = destination;
120
- sourceDocument = { kind: "source", source: originalSource, revision: "local", path: "oats-package.json" };
121
- } else {
122
- portablePath(packagePath, { allowRoot: true });
123
- const observation = observe(repository, selector, request.origin);
124
- repositories.materialize(observation, [packagePath], destination);
125
- payload = join(destination, packagePath); commit = observation.source.commit;
126
- repoAnchor = { source: observation.source.remote, revision: commit };
127
- sourceDocument = { kind: "source", source: observation.source.remote, revision: commit, path: posix.join(packagePath, "oats-package.json") };
128
- }
129
- // Strict JSON ingress precedes the sole existing full package/manifest codec.
130
- const bytes = readPortableBytes(join(payload, "oats-package.json")); parseStrictJson(bytes);
131
- sourceDocument.integrity = bytesIntegrity(bytes);
132
- const manifest = kernel.loadPackageManifestAt(payload, { strict: true });
133
- const dependencyRequests = (manifest.dependencies ?? []).map((spec, index) => {
134
- // Local siblings are authoring input. A remote manifest never gains access
135
- // to adopter files merely because another root request was local.
136
- let source = parsePackageDependency3(spec, { localBase, allowLocalPaths: allowLocalPaths && localBase !== undefined });
137
- if (source.kind === "repo") {
138
- if (!repoAnchor) throw oatsError("needs-configuration", "repo dependency has no explicit repository snapshot anchor");
139
- source = { ...source, anchor: validateRepoAnchor(repoAnchor) };
140
- }
141
- return { source, origin: { kind: "package-dependency", document: sourceDocument, pointer: `/dependencies/${index}` } };
142
- });
143
- const record = { package: manifest.package, version: manifest.version, source: originalSource, path: packagePath, commit,
144
- sourceKey: canonicalJson({ source: originalSource, path: packagePath, commit }), dir: payload, manifest,
145
- capabilities: manifest._capabilities, dependencyRequests, origins: [request.origin] };
146
- cached.set(key, record); return record;
147
- };
148
- try {
149
- const graph = resolvePackageClosure({ requests, readPackage,
150
- validatePackage(record) {
151
- const compatibility = kernel.capabilityCompatibility(record.manifest);
152
- if (!compatibility.compatible) throw oatsError("incompatible-oats", `package ${record.package} requires OATS ${compatibility.range}`);
153
- },
154
- finalizePackage: (record, deps) => ({ ...record, deps, integrity: treeIntegrity(record.dir, { format: PACKAGE_FORMAT }) }),
155
- discardPackage: () => {}, // All duplicate payloads belong to outer owned staging.
156
- });
157
- const owners = new Map();
158
- for (const record of graph.packages.values()) for (const cap of record.capabilities) {
159
- if (owners.has(cap.id) && owners.get(cap.id) !== record.package) throw oatsError("duplicate-capability-id", "selected package closures export conflicting capability identities");
160
- owners.set(cap.id, record.package);
161
- }
162
- requests.forEach((request, index) => {
163
- if (request.capability && !graph.packages.get(graph.roots[index]).capabilities.some((cap) => cap.id === request.capability)) throw oatsError("capability-list-mismatch", "selected source does not export the required capability");
164
- });
165
- kernel.assertPlatformInvariantLocks([...graph.packages.values()].flatMap((record) => record.capabilities.map((cap) => cap.dir)));
166
- const artifactSet = { schemaVersion: 1, packages: Object.create(null), capabilities: Object.create(null) };
167
- const artifacts = new Map();
168
- for (const record of graph.packages.values()) {
169
- artifactSet.packages[record.package] = { source: record.source, path: record.path, version: record.version, commit: record.commit,
170
- integrity: record.integrity, dependencies: [...new Set(record.deps)].sort(compareUtf8) };
171
- for (const cap of record.capabilities) {
172
- const projected = kernel.materializeCapability({ cap, pkg: record, artifactsDir: join(staging, "artifacts") });
173
- const artifact = { kind: "capability", capability: cap.id, integrity: treeIntegrity(projected.dir) };
174
- artifactSet.capabilities[cap.id] = { version: projected.version, artifact, origin: { kind: "package", package: record.package, path: cap.rel, projectionVersion: 1 } };
175
- artifacts.set(cap.id, projected.dir);
176
- }
177
- }
178
- validateArtifactSet(artifactSet);
179
- for (const [id, source] of artifacts) retainPortableArtifact(deployment, source, artifactSet.capabilities[id].artifact);
180
- return { artifactSet, roots: graph.roots, cleanup };
181
- } catch (error) {
182
- try { cleanup(); } catch (failure) {
183
- const combined = new AggregateError([error, failure], "package preparation and cleanup failed", { cause: error });
184
- combined.code = error.code; combined.stagingPath = staging; throw combined;
185
- }
186
- throw error;
187
- }
188
- }
@@ -1,44 +0,0 @@
1
- /** Shared authored selection shapes for the two policy authorities. Validation
2
- * only: no precedence, provider selection, acquisition, enrollment or execution. */
3
- import { isMaterializedCapabilityId } from "./capability-provenance.mjs";
4
- import { parsePortableSource } from "./source-spec.mjs";
5
- import { invalidShape, objectAt, pointerKey, stringAt } from "./portable-shape.mjs";
6
-
7
- export const FUNDAMENTAL_SLOTS = Object.freeze(["knowledge", "messaging", "tasks"]);
8
- export function validatePolicyChoices(value, { pointer, required = false, origins = {}, origin = null, localBase, allowLocalPaths = false }) {
9
- const rules = objectAt(value, ["capabilities", ...FUNDAMENTAL_SLOTS], [], pointer);
10
- const sources = Object.create(null);
11
- const capabilityAt = (id, at) => { if (!isMaterializedCapabilityId(id)) invalidShape(at, "invalid capability identity"); };
12
- const selection = (item, at, capability) => {
13
- objectAt(item, capability === undefined ? ["capability", "source", "settings"] : ["source", "settings"],
14
- capability === undefined ? ["capability", "source"] : ["source"], at);
15
- capabilityAt(capability ?? item.capability, capability === undefined ? `${at}/capability` : at);
16
- stringAt(item.source, `${at}/source`);
17
- try { sources[`${at}/source`] = parsePortableSource(item.source, { localBase, allowLocalPaths }); }
18
- catch (error) { error.provenance = [origins[`${at}/source`] ?? { document: origin, pointer: `${at}/source` }]; throw error; }
19
- if (Object.hasOwn(item, "settings")) objectAt(item.settings, null, [], `${at}/settings`);
20
- };
21
- if (Object.hasOwn(rules, "capabilities")) {
22
- objectAt(rules.capabilities, null, [], `${pointer}/capabilities`);
23
- for (const [id, item] of Object.entries(rules.capabilities)) {
24
- const at = `${pointer}/capabilities/${pointerKey(id)}`;
25
- capabilityAt(id, at);
26
- if (!required && item === false) continue;
27
- selection(item, at, id);
28
- }
29
- }
30
- for (const slot of FUNDAMENTAL_SLOTS) {
31
- if (!Object.hasOwn(rules, slot)) continue;
32
- const item = rules[slot], at = `${pointer}/${slot}`;
33
- if ((required && item === "any") || (!required && item === "none")) continue;
34
- selection(item, at);
35
- }
36
- return sources;
37
- }
38
-
39
- export function validateProviderDeclaration(value, pointer) {
40
- const envelope = objectAt(value, ["contract", "version", "payload"], ["contract", "version", "payload"], pointer);
41
- stringAt(envelope.contract, `${pointer}/contract`, { pattern: /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/ });
42
- if (!Number.isSafeInteger(envelope.version) || envelope.version < 1) invalidShape(`${pointer}/version`, "invalid provider contract version");
43
- return envelope;
44
- }
@@ -1,42 +0,0 @@
1
- /** Versioned soul declarations: parsing and origins, not composition or execution.
2
- * Requirements constrain the later single resolver; defaults never erase them. */
3
- import { parseConfigData } from "./config-data.mjs";
4
- import { portablePath } from "./source-spec.mjs";
5
- import { invalidShape, objectAt, stringAt, stringSetAt, versionAt } from "./portable-shape.mjs";
6
- import { validatePolicyChoices, validateProviderDeclaration } from "./portable-policy.mjs";
7
-
8
- export { FUNDAMENTAL_SLOTS } from "./portable-policy.mjs";
9
- const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
10
- const ALIAS = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
11
- const LAUNCH_CONFIG_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
12
- const ROOT_FIELDS = ["schemaVersion", "name", "description", "requires", "defaults", "knowledge", "teams", "resources", "work", "runtime", "model", "yolo", "launch-config", "backend", "children"];
13
-
14
- export function parsePortableSoul(input, { origin = null, localBase, allowLocalPaths = false, limits } = {}) {
15
- const parsed = parseConfigData(input, { origin, limits });
16
- const value = objectAt(parsed.value, ROOT_FIELDS, ["schemaVersion", "name"]);
17
- versionAt(value.schemaVersion);
18
- stringAt(value.name, "/name", { pattern: NAME });
19
- if (Object.hasOwn(value, "description")) stringAt(value.description, "/description", { empty: true });
20
- const sources = Object.create(null);
21
- for (const section of ["requires", "defaults"]) {
22
- if (!Object.hasOwn(value, section)) continue;
23
- Object.assign(sources, validatePolicyChoices(value[section], {
24
- pointer: `/${section}`, required: section === "requires", origins: parsed.origins,
25
- origin, localBase, allowLocalPaths,
26
- }));
27
- }
28
- if (Object.hasOwn(value, "knowledge")) validateProviderDeclaration(value.knowledge, "/knowledge");
29
- if (Object.hasOwn(value, "teams")) stringSetAt(value.teams, "/teams", (item, pointer) => stringAt(item, pointer, { pattern: ALIAS }));
30
- if (Object.hasOwn(value, "resources")) stringSetAt(value.resources, "/resources", (item) => portablePath(item, { allowRoot: true }));
31
- if (Object.hasOwn(value, "work") && !["worktree", "checkout", "attached", "workspace", "directory"].includes(value.work)) invalidShape("/work", "unsupported work mode");
32
- if (Object.hasOwn(value, "runtime") && !["pi", "claude", "codex"].includes(value.runtime)) invalidShape("/runtime", "unsupported runtime");
33
- if (Object.hasOwn(value, "model")) stringAt(value.model, "/model");
34
- if (Object.hasOwn(value, "launch-config")) stringAt(value["launch-config"], "/launch-config", { pattern: LAUNCH_CONFIG_NAME });
35
- if (Object.hasOwn(value, "backend") && !["tmux", "herdr"].includes(value.backend)) invalidShape("/backend", "unsupported session backend");
36
- if (Object.hasOwn(value, "yolo") && typeof value.yolo !== "boolean") invalidShape("/yolo", "expected boolean");
37
- if (Object.hasOwn(value, "children")) {
38
- const children = objectAt(value.children, ["spawn"], [], "/children");
39
- if (Object.hasOwn(children, "spawn") && typeof children.spawn !== "boolean") invalidShape("/children/spawn", "expected boolean");
40
- }
41
- return { declaration: value, sources, origins: parsed.origins, integrity: parsed.integrity };
42
- }
@@ -1,80 +0,0 @@
1
- /** Cooperative serialization for new portable-state writers, not legacy cutover
2
- * or hostile-host locking. A crashed guard is held for explicit recovery. */
3
- import { linkSync, lstatSync, mkdirSync, mkdtempSync, realpathSync, renameSync, rmSync, rmdirSync, writeFileSync } from "node:fs";
4
- import { dirname, isAbsolute, join } from "node:path";
5
- import { canonicalJson } from "./portable-values.mjs";
6
- import { ensureStoreIgnore } from "./capability-artifacts.mjs";
7
- import { oatsError } from "./errors.mjs";
8
-
9
- export function portableScope(scope) {
10
- if (typeof scope !== "string" || !isAbsolute(scope)) throw oatsError("invalid-artifact-store", "portable deployment must be explicit and absolute");
11
- const actual = realpathSync(scope);
12
- if (!lstatSync(actual).isDirectory()) throw oatsError("invalid-artifact-store", "portable deployment must be a directory");
13
- return actual;
14
- }
15
- export function portableStateDirectory(scope, create = false) {
16
- let root = portableScope(scope);
17
- for (const name of [".agents", "portable"]) {
18
- root = join(root, name);
19
- if (create) { try { mkdirSync(root, { mode: 0o700 }); } catch (error) { if (error.code !== "EEXIST") throw error; } }
20
- let stat;
21
- try { stat = lstatSync(root); } catch (error) { if (!create && error.code === "ENOENT") return null; throw error; }
22
- if (!stat.isDirectory()) throw oatsError("invalid-artifact-store", "managed portable-state component is not a real directory");
23
- }
24
- if (create) ensureStoreIgnore(root);
25
- return root;
26
- }
27
- const activeWrites = new WeakSet();
28
-
29
- /** Only an active synchronous write can publish a mutable metadata document. */
30
- export function writeGuardedPortableDocument(context, target, value, { absent = false } = {}) {
31
- if (!activeWrites.has(context)) throw oatsError("selection-changed", "portable write context is not active");
32
- const { root, deployment } = context;
33
- if (dirname(target) !== root && target !== join(deployment, "oats-lock.json")) throw oatsError("invalid-artifact-store", "portable write target is outside its owned metadata namespace");
34
- const bytes = canonicalJson(value), staging = mkdtempSync(join(root, ".document-")), candidate = join(staging, "document.json");
35
- let failed = false, primary;
36
- try {
37
- writeFileSync(candidate, bytes, { flag: "wx", mode: 0o600 });
38
- if (absent) {
39
- try { linkSync(candidate, target); }
40
- catch (error) { if (error.code === "EEXIST") throw oatsError("selection-changed", "portable metadata appeared during first publication"); throw error; }
41
- } else renameSync(candidate, target);
42
- } catch (error) { failed = true; primary = error; throw error; }
43
- finally {
44
- try { rmSync(staging, { recursive: true, force: true }); }
45
- catch (cleanup) {
46
- const error = failed ? new AggregateError([primary, cleanup], "portable metadata write and staging cleanup failed", { cause: primary })
47
- : new Error("portable metadata staging cleanup failed", { cause: cleanup });
48
- error.code = failed ? primary.code : cleanup.code; error.stagingPath = staging; throw error;
49
- }
50
- }
51
- }
52
-
53
- export function withPortableStateWrite(scope, write) {
54
- const deployment = portableScope(scope), root = portableStateDirectory(deployment, true), guard = join(root, ".write-guard");
55
- try { mkdirSync(guard, { mode: 0o700 }); }
56
- catch (error) {
57
- if (error.code === "EEXIST") throw oatsError("selection-changed", "portable state has another writer or a held recovery guard");
58
- throw error;
59
- }
60
- const owned = lstatSync(guard), context = Object.freeze({ deployment, root });
61
- activeWrites.add(context);
62
- let failed = false, primary;
63
- try { return write(context); }
64
- catch (error) { failed = true; primary = error; throw error; }
65
- finally {
66
- activeWrites.delete(context);
67
- try {
68
- const current = lstatSync(guard);
69
- if (!current.isDirectory() || current.dev !== owned.dev || current.ino !== owned.ino) throw oatsError("selection-changed", "portable write guard ownership changed");
70
- // Empty owned guard only; never force-remove a foreign/stale guard tree.
71
- rmdirSync(guard);
72
- } catch (cleanup) {
73
- const error = failed ? new AggregateError([primary, cleanup], "portable state write and guard cleanup failed", { cause: primary })
74
- : new Error("portable write guard cleanup failed", { cause: cleanup });
75
- error.code = failed ? primary.code : cleanup.code;
76
- error.guardPath = guard;
77
- throw error;
78
- }
79
- }
80
- }