@b00dea/cockpit 0.1.0 → 0.1.1

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 (196) hide show
  1. package/CHANGELOG.md +99 -0
  2. package/README.md +3 -0
  3. package/cockpit.schema.json +73 -3
  4. package/dist/autonomy.d.ts +3 -2
  5. package/dist/autonomy.js +30 -13
  6. package/dist/browser/commands.d.ts +57 -2
  7. package/dist/browser/commands.js +114 -10
  8. package/dist/browser/runtime.d.ts +1 -0
  9. package/dist/browser/runtime.js +4 -1
  10. package/dist/browser/surfaces.d.ts +7 -7
  11. package/dist/browser/surfaces.js +5 -0
  12. package/dist/canonical-json.d.ts +2 -0
  13. package/dist/canonical-json.js +5 -0
  14. package/dist/cli-main.d.ts +40 -3
  15. package/dist/cli-main.js +241 -62
  16. package/dist/command-table.d.ts +19 -11
  17. package/dist/command-table.js +190 -38
  18. package/dist/config/load.d.ts +16 -4
  19. package/dist/config/load.js +25 -3
  20. package/dist/config/unconfirmed-toml.d.ts +9 -0
  21. package/dist/config/unconfirmed-toml.js +29 -0
  22. package/dist/diagnostics.js +10 -1
  23. package/dist/docs/check.d.ts +16 -0
  24. package/dist/docs/check.js +197 -0
  25. package/dist/docs/decision-record.d.ts +34 -0
  26. package/dist/docs/decision-record.js +135 -0
  27. package/dist/docs/decisions.d.ts +16 -0
  28. package/dist/docs/decisions.js +103 -0
  29. package/dist/docs/markdown.d.ts +36 -0
  30. package/dist/docs/markdown.js +88 -0
  31. package/dist/docs/references.d.ts +32 -0
  32. package/dist/docs/references.js +148 -0
  33. package/dist/environment/application/confirm.d.ts +35 -0
  34. package/dist/environment/application/confirm.js +122 -0
  35. package/dist/environment/application/destructive.js +6 -2
  36. package/dist/environment/application/doctor.d.ts +12 -7
  37. package/dist/environment/application/doctor.js +20 -8
  38. package/dist/environment/application/guard.d.ts +12 -6
  39. package/dist/environment/application/guard.js +87 -32
  40. package/dist/environment/application/health.d.ts +14 -5
  41. package/dist/environment/application/health.js +8 -2
  42. package/dist/environment/application/lifecycle-context.d.ts +10 -3
  43. package/dist/environment/application/lifecycle-state.js +4 -3
  44. package/dist/environment/application/lifecycle.d.ts +2 -1
  45. package/dist/environment/application/lifecycle.js +11 -3
  46. package/dist/environment/application/services.d.ts +60 -0
  47. package/dist/environment/application/services.js +89 -0
  48. package/dist/environment/application/siblings.d.ts +43 -0
  49. package/dist/environment/application/siblings.js +269 -0
  50. package/dist/environment/claims.d.ts +10 -10
  51. package/dist/environment/claims.js +3 -11
  52. package/dist/environment/compose-topology.d.ts +15 -7
  53. package/dist/environment/compose-topology.js +100 -23
  54. package/dist/environment/contract.d.ts +42 -9
  55. package/dist/environment/contract.js +33 -0
  56. package/dist/environment/executable.d.ts +12 -0
  57. package/dist/environment/executable.js +46 -23
  58. package/dist/environment/generated-application/check.js +2 -0
  59. package/dist/environment/generated-application/prepare.js +2 -0
  60. package/dist/environment/generated-application/shared.d.ts +10 -2
  61. package/dist/environment/generated-application/shared.js +6 -1
  62. package/dist/environment/generated-application/surface-outputs.js +23 -12
  63. package/dist/environment/generated-application/surfaces.js +16 -11
  64. package/dist/environment/generated-application/sync.js +19 -4
  65. package/dist/environment/git-observation.d.ts +25 -6
  66. package/dist/environment/git-observation.js +37 -28
  67. package/dist/environment/health-composition.d.ts +3 -2
  68. package/dist/environment/health-composition.js +19 -2
  69. package/dist/environment/init-application.d.ts +39 -14
  70. package/dist/environment/init-application.js +39 -10
  71. package/dist/environment/lifecycle-composition.d.ts +4 -2
  72. package/dist/environment/lifecycle-composition.js +84 -17
  73. package/dist/environment/lifecycle-stage.d.ts +8 -3
  74. package/dist/environment/lifecycle-stage.js +11 -1
  75. package/dist/environment/logs.d.ts +1 -1
  76. package/dist/environment/map-limit.d.ts +2 -0
  77. package/dist/environment/map-limit.js +13 -0
  78. package/dist/environment/mcp-observation.d.ts +9 -0
  79. package/dist/environment/mcp-observation.js +46 -0
  80. package/dist/environment/observations.d.ts +1 -1
  81. package/dist/environment/observations.js +2 -0
  82. package/dist/environment/operator-application.d.ts +35 -6
  83. package/dist/environment/operator-application.js +42 -0
  84. package/dist/environment/presence.d.ts +4 -0
  85. package/dist/environment/presence.js +26 -0
  86. package/dist/environment/read-composition.d.ts +8 -3
  87. package/dist/environment/read-composition.js +53 -8
  88. package/dist/environment/readiness-composition.d.ts +20 -1
  89. package/dist/environment/readiness-composition.js +23 -2
  90. package/dist/environment/reclaim.d.ts +56 -19
  91. package/dist/environment/reclaim.js +147 -42
  92. package/dist/environment/runtime.d.ts +64 -18
  93. package/dist/environment/runtime.js +73 -3
  94. package/dist/environment/served-checkout-grants.d.ts +53 -0
  95. package/dist/environment/served-checkout-grants.js +182 -0
  96. package/dist/environment/sibling-checkouts.d.ts +119 -0
  97. package/dist/environment/sibling-checkouts.js +254 -0
  98. package/dist/environment/sibling-observation.d.ts +27 -0
  99. package/dist/environment/sibling-observation.js +45 -0
  100. package/dist/environment/sibling-pull.d.ts +23 -0
  101. package/dist/environment/sibling-pull.js +95 -0
  102. package/dist/environment/stack-inputs.d.ts +18 -0
  103. package/dist/environment/stack-inputs.js +210 -0
  104. package/dist/environment/stack-inventory-prune.d.ts +31 -0
  105. package/dist/environment/stack-inventory-prune.js +129 -0
  106. package/dist/environment/stack-inventory-report.d.ts +44 -0
  107. package/dist/environment/stack-inventory-report.js +78 -0
  108. package/dist/environment/stack-inventory.d.ts +94 -0
  109. package/dist/environment/stack-inventory.js +418 -0
  110. package/dist/environment/state.d.ts +48 -9
  111. package/dist/environment/state.js +142 -2
  112. package/dist/environment/tool-registration.js +3 -7
  113. package/dist/environment-exec/execute.d.ts +6 -3
  114. package/dist/environment-exec/execute.js +4 -0
  115. package/dist/fix-catalogue.d.ts +1 -1
  116. package/dist/generated-environment/adopt.js +4 -8
  117. package/dist/guards/answer-confirmation.d.ts +41 -0
  118. package/dist/guards/answer-confirmation.js +109 -0
  119. package/dist/guards/evaluator.d.ts +20 -3
  120. package/dist/guards/evaluator.js +39 -7
  121. package/dist/guards/local-identity.d.ts +6 -2
  122. package/dist/guards/local-identity.js +10 -4
  123. package/dist/guards/policy-change.d.ts +43 -0
  124. package/dist/guards/policy-change.js +159 -0
  125. package/dist/guards/policy-pin.d.ts +1 -1
  126. package/dist/guards/policy-pin.js +14 -12
  127. package/dist/init/command.d.ts +4 -3
  128. package/dist/init/command.js +36 -16
  129. package/dist/init/derive.d.ts +8 -6
  130. package/dist/init/derive.js +140 -36
  131. package/dist/init/draft.d.ts +13 -11
  132. package/dist/init/draft.js +54 -31
  133. package/dist/init/render.d.ts +18 -6
  134. package/dist/init/render.js +31 -9
  135. package/dist/repository-path.d.ts +9 -0
  136. package/dist/repository-path.js +16 -0
  137. package/dist/schema.d.ts +42 -75
  138. package/dist/schema.js +62 -11
  139. package/dist/surfaces/harness-registry.d.ts +80 -0
  140. package/dist/surfaces/harness-registry.js +29 -0
  141. package/dist/surfaces/mcp.d.ts +67 -17
  142. package/dist/surfaces/mcp.js +194 -31
  143. package/dist/surfaces/projection.d.ts +7 -7
  144. package/dist/surfaces/projection.js +41 -6
  145. package/dist/surfaces/render.d.ts +5 -44
  146. package/dist/surfaces/render.js +5 -2
  147. package/dist/team-metadata.js +2 -1
  148. package/dist/testing/test-protection.d.ts +34 -0
  149. package/dist/testing/test-protection.js +137 -0
  150. package/dist/testing/weakening.d.ts +15 -0
  151. package/dist/testing/weakening.js +30 -0
  152. package/dist/work-orders/application-closure.d.ts +7 -0
  153. package/dist/work-orders/application-closure.js +76 -0
  154. package/dist/work-orders/application-context.d.ts +162 -0
  155. package/dist/work-orders/application-context.js +137 -0
  156. package/dist/work-orders/application-decisions.d.ts +27 -0
  157. package/dist/work-orders/application-decisions.js +121 -0
  158. package/dist/work-orders/application-grading.d.ts +11 -0
  159. package/dist/work-orders/application-grading.js +212 -0
  160. package/dist/work-orders/application.d.ts +4 -97
  161. package/dist/work-orders/application.js +94 -254
  162. package/dist/work-orders/composition.d.ts +35 -58
  163. package/dist/work-orders/composition.js +216 -24
  164. package/dist/work-orders/contract.d.ts +22 -6
  165. package/dist/work-orders/contract.js +34 -16
  166. package/dist/work-orders/decisions.d.ts +23 -0
  167. package/dist/work-orders/decisions.js +55 -0
  168. package/dist/work-orders/store.d.ts +54 -5
  169. package/dist/work-orders/store.js +118 -24
  170. package/docs/README.md +6 -3
  171. package/docs/capability-matrix.md +17 -9
  172. package/docs/cli-reference.md +209 -1
  173. package/docs/command-list.md +12 -1
  174. package/docs/concepts.md +6 -4
  175. package/docs/configuration-essentials.md +7 -2
  176. package/docs/configuration-reference.md +18 -3
  177. package/docs/docs-check.md +76 -0
  178. package/docs/environment.md +194 -6
  179. package/docs/faq.md +5 -3
  180. package/docs/getting-started.md +109 -25
  181. package/docs/harness-surfaces.md +69 -2
  182. package/docs/safety-and-autonomy.md +56 -12
  183. package/docs/troubleshooting.md +29 -8
  184. package/docs/tutorial.md +3 -2
  185. package/docs/work-and-browser.md +180 -14
  186. package/libexec/browser-operation.cjs +86 -2
  187. package/package.json +2 -2
  188. package/surfaces/skills/cockpit-browser.skill.md +7 -2
  189. package/surfaces/skills/cockpit-explain.skill.md +1 -1
  190. package/surfaces/skills/cockpit-fix.skill.md +1 -1
  191. package/surfaces/skills/cockpit-setup.skill.md +3 -3
  192. package/surfaces/skills/cockpit-work.skill.md +1 -1
  193. package/surfaces/skills/procedures/explain.md +9 -4
  194. package/surfaces/skills/procedures/fix.md +25 -11
  195. package/surfaces/skills/procedures/setup.md +113 -40
  196. package/surfaces/skills/procedures/work.md +45 -16
@@ -288,9 +288,14 @@ export function complete(action, prepared, dryRun = false, changes = [], staleRe
288
288
  })),
289
289
  ...prepared.unapprovedMcpServers.map(server => ({
290
290
  severity: 'warning', code: 'MCP_NOT_APPROVED', target: server.name,
291
- message: `MCP server ${server.name}, declared in ${server.sources.join(', ')}, has no approval in [mcp.approved] for its current command, arguments, working directory and environment names (sha256 ${server.sha256}), so every harness keeps it disabled`,
291
+ message: `MCP server ${server.name}, declared in ${server.sources.join(', ')}, has no approval in [mcp.approved] for its current declaration (command, arguments, working directory and environment names for a local server; URL, header references and headers helper for a remote one) (sha256 ${server.sha256}), so every harness keeps it disabled`,
292
292
  fix: 'a person reviews its declaration, sets its name to the sha256 in this message under [mcp.approved] in cockpit.toml and runs npx --no cockpit sync --config <your cockpit.toml>; while [autonomy] mcp_approve is person, the default, the guard refuses an agent\'s edit of [mcp]. To keep it off, nothing is needed',
293
293
  })),
294
+ ...prepared.mcpTransportGaps.map(gap => ({
295
+ severity: 'warning', code: 'MCP_TRANSPORT_UNAVAILABLE', target: gap.name,
296
+ message: `${gap.reason}, so sync writes nothing for it there and the server is unavailable in those tools`,
297
+ fix: 'nothing is needed if the server is not wanted in those tools; otherwise declare it with credentials the tool can carry (an environment reference in headers rather than a headersHelper for OpenCode), or remove those tools from [surfaces] tools, then run npx --no cockpit sync --config <your cockpit.toml>',
298
+ })),
294
299
  // Real sync and check refuse a changed original; a preview reports it so it can be reviewed.
295
300
  ...prepared.changedOverrideOriginals.map(finding => overrideChanged(finding, 'warning')),
296
301
  ],
@@ -228,13 +228,14 @@ function openCodeExternalDirectories(grants) {
228
228
  // denied even `test-run` from its own skill. A rule only lets Claude run the command without asking:
229
229
  // the guard hook still runs first, and its ask or deny wins over the rule, so a command whose
230
230
  // decider the [work.gates] or [autonomy] policy makes a person still asks. Commands that are always
231
- // a person's (`init`, `sibling add`) and the destructive `down` are left out so they keep asking. Measured with Claude Code 2.1.316 on 2026-11-02 beside an unruled control: `Bash(<prefix> *)`
231
+ // a person's (`init`, `sibling add`) are left out so they keep asking; `down` keeps the data volumes. Measured with Claude Code 2.1.316 on 2026-11-02 beside an unruled control: `Bash(<prefix> *)`
232
232
  // ran a matching command headless, the control and a chain with an unmatched part were denied, a hook
233
233
  // `ask` denied a matching command, and Claude ignores these rules until a person trusts the workspace.
234
234
  const AGENT_COCKPIT_COMMANDS = [
235
- 'check *', 'doctor *', 'preflight *', 'logs *', 'sync *', 'reset * --apply', 'up *', 'sleep *', 'runtime measure *', 'test-run *',
236
- 'work status *', 'work accept *', 'work check *', 'work note *', 'work answer *', 'work grade *', 'work signoff *', 'work recover *',
237
- 'work verify *', 'grade *', 'upgrade *',
235
+ 'check *', 'confirm *', 'doctor *', 'preflight *', 'logs *', 'ps *', 'restart *', 'rebuild *', 'exec *', 'sync *', 'reset * --apply', 'up *', 'sleep *', 'down *',
236
+ 'runtime measure *', 'test-run *',
237
+ 'work status *', 'work accept *', 'work check *', 'work note *', 'work decide *', 'work answer *', 'work acknowledge *', 'work grade *', 'work signoff *', 'work recover *',
238
+ 'work verify *', 'grade *', 'upgrade *', 'docs check *', 'use *', 'pull *', 'stacks *',
238
239
  'browser drive *', 'browser screenshot *',
239
240
  ];
240
241
  function claudeCommandAllowances() {
@@ -251,15 +252,25 @@ export const GUIDE_LOAD_CHARACTERIZATIONS = Object.freeze(Object.fromEntries(shi
251
252
  }])));
252
253
  export function nativeSources(mcp, grants, siblings, instances) {
253
254
  const sources = [];
254
- if (mcp.result.servers.length > 0) {
255
- const transports = mcp.result.harnesses.flatMap(row => row.servers);
256
- const servers = Object.fromEntries(mcp.result.servers.map(model => transports.find(server => server.name === model.name)).map(server => [server.name, {
257
- type: 'stdio', command: server.command, args: server.args, disabled: !server.enabled,
258
- }]));
255
+ const transports = mcp.result.harnesses.flatMap(row => row.servers);
256
+ // A remote server no listed tool can carry is left out of the source entirely.
257
+ const written = mcp.result.servers.flatMap(model => transports.filter(server => server.name === model.name).slice(0, 1));
258
+ if (written.length > 0) {
259
+ const native = (server, helperKey) => 'url' in server
260
+ ? { type: 'http', url: server.url, ...(Object.keys(server.headers).length > 0 ? { headers: server.headers } : {}),
261
+ ...(server.headersHelper === null ? {} : { [helperKey]: server.headersHelper }), disabled: !server.enabled }
262
+ : { type: 'stdio', command: server.command, args: server.args, disabled: !server.enabled };
263
+ const servers = Object.fromEntries(written.map(server => [server.name, native(server, 'headersHelper')]));
259
264
  const targets = Object.fromEntries(mcp.result.harnesses.flatMap(row => {
260
- const removed = mcp.result.servers.filter(server => !row.servers.some(transport => transport.name === server.name));
261
- return removed.length ? [[harnessFact(row.target).rulesyncTarget, {
262
- mcpServers: Object.fromEntries(removed.map(server => [server.name, null])),
265
+ const removed = written.filter(server => !row.servers.some(transport => transport.name === server.name))
266
+ .map(server => [server.name, null]);
267
+ // Codex spells Claude Code's headersHelper http_headers_helper; Rulesync passes an unknown
268
+ // key through to config.toml unchanged.
269
+ const respelled = row.target === 'codexcli'
270
+ ? row.servers.filter(server => 'url' in server && server.headersHelper !== null).map(server => [server.name, native(server, 'http_headers_helper')])
271
+ : [];
272
+ return removed.length + respelled.length > 0 ? [[harnessFact(row.target).rulesyncTarget, {
273
+ mcpServers: Object.fromEntries([...removed, ...respelled]),
263
274
  }]] : [];
264
275
  }));
265
276
  sources.push({ path: 'mcp.jsonc', bytes: `${JSON.stringify({ mcpServers: servers, ...targets }, null, 2)}\n` });
@@ -1,4 +1,5 @@
1
1
  import { lstat, realpath } from 'node:fs/promises';
2
+ import { openCodePatternUnsafe } from '../served-checkout-grants.js';
2
3
  import { basename, dirname, join, relative, resolve } from 'node:path';
3
4
  import { fileURLToPath } from 'node:url';
4
5
  import { renderBrowserSurfaces } from '../../browser/surfaces.js';
@@ -22,7 +23,7 @@ import { fileFingerprint, verifiedDigest } from '../../environment-exec/verified
22
23
  import { SHIPPED_SETUP_SKILL, extendInspectedSkillStandingCosts, inspectRenderedSkillSet } from '../../surfaces/skill-contract.js';
23
24
  import { readAnchoredFile, readAnchoredEntry, readAnchoredBatches } from '../../generated-environment/transaction.js';
24
25
  import { ENVIRONMENT_CODES, errorCause, snapshotGeneratedJson, RefusalError } from '../contract.js';
25
- import { SIBLING_DEFAULT_GUIDES } from '../../schema.js';
26
+ import { mounted, SIBLING_DEFAULT_GUIDES } from '../../schema.js';
26
27
  import { checkoutId, cockpitId, stackId } from '../identity.js';
27
28
  import { resolveLifecycleStage } from '../lifecycle-stage.js';
28
29
  import { observeConfiguration, selectStateRoot } from '../state.js';
@@ -37,7 +38,9 @@ export async function prepareSurfaces(prepared, priorOwned, adapters, manifest)
37
38
  // Only the listed tools are rendered and drift-checked; an unlisted tool's files become stale.
38
39
  const tools = listedSurfaceTargets(config.surfaces.tools);
39
40
  const guardedTools = GUARD_SURFACE_TARGETS.filter(target => tools.includes(target));
40
- const selected = config.sibling.filter(row => row.mount === 'always');
41
+ // Generated files are one per checkout, so they follow the declared default profile set; a
42
+ // profile one run adds with `up --profile` reaches the stack, not the agents' instructions.
43
+ const selected = mounted(config, config.environment.profiles);
41
44
  const requiredSlugs = new Set([...selected.map(row => row.slug), ...prepared.registry.mcpSources.map(row => row.slug)]);
42
45
  const siblingRoots = new Map();
43
46
  const sourceRoots = [];
@@ -70,7 +73,8 @@ export async function prepareSurfaces(prepared, priorOwned, adapters, manifest)
70
73
  }))).sort((left, right) => compareCodeUnits(left.sourcePath, right.sourcePath));
71
74
  const observedGuides = guideRequests.length === 0
72
75
  ? { kind: 'complete', observations: [], missing: [] }
73
- : await observeSiblingGuides({ allowedRoots: selected.map(row => siblingRoots.get(row.slug)), requests: guideRequests });
76
+ : await observeSiblingGuides({ allowedRoots: selected.map(row => siblingRoots.get(row.slug)), requests: guideRequests,
77
+ instances: prepared.registry.secretPaths.flatMap(row => siblingRoots.has(row.slug) ? [resolve(siblingRoots.get(row.slug), row.path)] : []) });
74
78
  if (observedGuides.kind !== 'complete')
75
79
  return refuse(observedGuides.diagnostic.code, observedGuides.diagnostic.message, observedGuides.diagnostic.target);
76
80
  for (const sibling of selected)
@@ -82,7 +86,7 @@ export async function prepareSurfaces(prepared, priorOwned, adapters, manifest)
82
86
  const projectionInput = {
83
87
  canonicalMainCheckout: prepared.root,
84
88
  siblingsRoot: config.cockpit.siblings_root,
85
- activeProfiles: [],
89
+ activeProfiles: [...config.environment.profiles],
86
90
  guideCapLines: config.cockpit.guide_cap,
87
91
  siblings: config.sibling.map(row => ({ slug: row.slug, mount: row.mount, directoryAuthority: row.dir, guides: [...projectionGuides(row)] })),
88
92
  mountedPaths: selected.map(row => ({ slug: row.slug, path: siblingRoots.get(row.slug) })),
@@ -131,6 +135,8 @@ export async function prepareSurfaces(prepared, priorOwned, adapters, manifest)
131
135
  return refuse(mcp.diagnostic.code, mcp.diagnostic.message, mcp.diagnostic.server);
132
136
  const unapprovedMcpServers = mcp.diagnostics.filter(row => row.code === 'MCP_NOT_APPROVED' && row.server !== undefined)
133
137
  .map(row => ({ name: row.server, sha256: row.sha256, sources: (row.sources ?? []).map(source => source.kind === 'sibling' ? `${source.slug}:${source.path}` : source.path) }));
138
+ const mcpTransportGaps = mcp.diagnostics.filter(row => row.code === 'MCP_TRANSPORT_UNAVAILABLE' && row.server !== undefined)
139
+ .map(row => ({ name: row.server, targets: row.targets ?? [], reason: row.message }));
134
140
  if (mcp.diagnostics.some(row => row.code === 'MCP_APPROVAL_UNKNOWN')) {
135
141
  const row = mcp.diagnostics.find(candidate => candidate.code === 'MCP_APPROVAL_UNKNOWN');
136
142
  return refuse(row.code, row.message, row.server);
@@ -154,9 +160,10 @@ export async function prepareSurfaces(prepared, priorOwned, adapters, manifest)
154
160
  if (!checkout)
155
161
  return refuse('SURFACE_INPUT_INVALID', 'browser checkout identity is unavailable', prepared.configPath);
156
162
  const browserConfiguration = prepared.workEnvironment.browser;
157
- const playwright = declarations.filter(row => row.name === 'playwright');
158
- if (playwright.length !== 1) {
159
- return refuse('MCP_DECLARATION_INVALID', `[browser] requires exactly one MCP server named playwright, and the declared MCP sources hold ${playwright.length}`, 'playwright', 'declare one playwright server in a JSON file with an mcpServers object, listed in a sibling\'s derives.mcp_sources, then run npx --no cockpit sync --config <your cockpit.toml>');
163
+ const playwright = declarations.filter(row => row.name === 'playwright')
164
+ .flatMap(row => 'command' in row ? [row] : []);
165
+ if (playwright.length !== 1 || declarations.filter(row => row.name === 'playwright').length !== 1) {
166
+ return refuse('MCP_DECLARATION_INVALID', `[browser] requires exactly one local-command MCP server named playwright, and the declared MCP sources hold ${declarations.filter(row => row.name === 'playwright').length} servers by that name, ${playwright.length} of them local`, 'playwright', 'declare one playwright server with a command (not a url) in a JSON file with an mcpServers object, listed in a sibling\'s derives.mcp_sources, then run npx --no cockpit sync --config <your cockpit.toml>');
160
167
  }
161
168
  const playwrightDigest = mcpApprovalDigest(playwright[0], prepared.root);
162
169
  if (config.mcp.approved.playwright !== playwrightDigest) {
@@ -627,12 +634,10 @@ export async function prepareSurfaces(prepared, priorOwned, adapters, manifest)
627
634
  };
628
635
  const openCodeGrantRoots = projection.grants.filter(grant => grant.target === 'opencode')
629
636
  .map(grant => ({ slug: grant.slug, root: siblingRoots.get(grant.slug) }));
630
- // The file's bytes become part of an OpenCode pattern: a wildcard would widen the grant and a
631
- // quote or backslash would change the JSON around it.
632
- const unsafeGrant = openCodeGrantRoots.find(row => row.root === undefined || /[*?"\\\x00-\x1f\x7f]/.test(row.root));
637
+ const unsafeGrant = openCodeGrantRoots.find(row => row.root === undefined || openCodePatternUnsafe(row.root) !== undefined);
633
638
  if (unsafeGrant)
634
639
  return refuse('SURFACE_INPUT_INVALID', `the directory of sibling ${unsafeGrant.slug} contains *, ?, a quote, a backslash or a control character, which OpenCode would read as part of its access pattern; move the checkout to a plain path`, unsafeGrant.slug);
635
- return { ...prepared, guardHandler, openCodeGrantRoots, unapprovedMcpServers,
640
+ return { ...prepared, guardHandler, openCodeGrantRoots, unapprovedMcpServers, mcpTransportGaps, mcpServers: mcp.result.servers,
636
641
  surfaces: surfaceEntries, compactSurfacePaths, configuredHarnesses,
637
642
  activeSurfaceOverrides: merge.overrides, changedOverrideOriginals: merge.findings,
638
643
  surfaceSelftest,
@@ -3,11 +3,14 @@ import { basename } from 'node:path';
3
3
  import { GUARD_LOCAL_PATHS, writeCockpitLocalFiles, writeGuardLocalIdentity } from '../../guards/local-identity.js';
4
4
  import { portablePublicationPaths, authorizePublicationPlan, runPublicationPlan } from '../../generated-environment/transaction.js';
5
5
  import { refusalOutcome as refuse } from '../../refusal.js';
6
+ import { readBoundedInput } from '../../config/load.js';
6
7
  import { complete, mergedAdapters, overrideChangedRefusal, ownRecord, sha256, } from './shared.js';
7
8
  import { syncCommandInput } from './prepare.js';
8
9
  import { inputSourcesCurrent, ownedInputOverlap, preparePublication } from './publication.js';
9
10
  import { checkCanonicalGeneratedEnvironment, issueRepairAuthority, issuedRepairs } from './check.js';
10
11
  import { openCodeGrantFile } from './surface-outputs.js';
12
+ import { readSiblingCheckouts } from '../sibling-checkouts.js';
13
+ import { openCodeGrantDirectory, openCodePatternUnsafe } from '../served-checkout-grants.js';
11
14
  async function prepareGeneratedEnvironmentSync(input, adapterOverrides, metadataChanges = []) {
12
15
  const request = syncCommandInput(input);
13
16
  if (!request)
@@ -94,14 +97,26 @@ export async function applyGeneratedEnvironmentRepair(authority, options = {}) {
94
97
  if (outcome.kind === 'refusal')
95
98
  return outcome;
96
99
  if (prepared.guardHandler && settings.dryRun !== true) {
97
- const local = await writeGuardLocalIdentity(prepared.root, prepared.guardHandler);
100
+ // The guard compares a later edit with exactly the text this identity names, so it is read
101
+ // again and must still be that text.
102
+ const configRead = await readBoundedInput(prepared.configPath);
103
+ if (configRead.kind !== 'complete' || sha256(configRead.bytes) !== prepared.guardHandler.configSha256)
104
+ return refuse('GENERATED_SOURCE_CHANGED', 'the configuration changed after generated files were published, so the guard identity was not recorded; the guard refuses every agent tool call until it is', prepared.configSource.path, 'run `npx --no cockpit sync --config <your cockpit.toml>` again');
105
+ const local = await writeGuardLocalIdentity(prepared.root, prepared.guardHandler, configRead.bytes.toString('utf8'));
98
106
  if (local.kind === 'refusal')
99
107
  return refuse('GENERATED_PUBLICATION_FAILED', `the committed guard files were published, but ${local.message}; the guard refuses every agent tool call until it exists`, GUARD_LOCAL_PATHS.identity, 'fix the path, then run `npx --no cockpit sync --config <your cockpit.toml>` again in a terminal');
100
108
  }
101
109
  if (prepared.openCodeGrantRoots.length > 0 && settings.dryRun !== true) {
102
- const local = await writeCockpitLocalFiles(prepared.root, prepared.openCodeGrantRoots.map(row => ({
103
- name: basename(openCodeGrantFile(row.slug)), bytes: row.root
104
- })));
110
+ // A checkout `use` serves from outside the declared one is what OpenCode is granted, as `use` wrote it.
111
+ const recorded = await readSiblingCheckouts(prepared.root);
112
+ if (recorded.kind !== 'complete')
113
+ return refuse(recorded.diagnostic.code, `the committed OpenCode settings were published, but its grants were not written: ${recorded.diagnostic.message}`, recorded.diagnostic.target, recorded.diagnostic.fix);
114
+ const grants = prepared.openCodeGrantRoots.map(row => ({ name: basename(openCodeGrantFile(row.slug)),
115
+ bytes: openCodeGrantDirectory(row.root, Object.hasOwn(recorded.checkouts, row.slug) ? recorded.checkouts[row.slug] : undefined) }));
116
+ const unsafe = grants.map(row => openCodePatternUnsafe(row.bytes)).find(reason => reason !== undefined);
117
+ if (unsafe)
118
+ return refuse('GENERATED_PUBLICATION_FAILED', `the committed OpenCode settings were published, but its grants were not written: ${unsafe}`, GUARD_LOCAL_PATHS.directory, 'serve the sibling from a checkout at a plain path with `npx --no cockpit use`, then run sync again');
119
+ const local = await writeCockpitLocalFiles(prepared.root, grants);
105
120
  if (local.kind === 'refusal')
106
121
  return refuse('GENERATED_PUBLICATION_FAILED', `the committed OpenCode settings were published, but ${local.message}; OpenCode refuses to start until it exists`, GUARD_LOCAL_PATHS.directory, 'fix the path, then run `npx --no cockpit sync --config <your cockpit.toml>` again in a terminal');
107
122
  }
@@ -49,7 +49,7 @@ export declare const observationDeclarations: readonly [{
49
49
  }];
50
50
  declare function isGitObservation(value: unknown): value is EnvironmentObservation;
51
51
  declare function observationIssuedAt(value: unknown): number | undefined;
52
- export interface GitObservationEvidence {
52
+ interface GitObservationEvidence {
53
53
  readonly revision: string | null;
54
54
  readonly branch: string;
55
55
  readonly upstream: string | null;
@@ -60,7 +60,7 @@ export interface GitObservationEvidence {
60
60
  readonly workingTree: 'clean' | 'dirty' | 'untracked' | 'dirty-and-untracked';
61
61
  readonly adjacency: 'aligned' | 'ahead' | 'behind' | 'diverged' | 'untracked-upstream' | 'upstream-gone';
62
62
  }
63
- export type GitObservationResult = {
63
+ type GitObservationResult = {
64
64
  readonly kind: 'complete';
65
65
  readonly evidence: GitObservationEvidence;
66
66
  readonly observations: readonly EnvironmentObservation[];
@@ -77,6 +77,25 @@ export declare function createGitObservationStep(input: {
77
77
  readonly source: string;
78
78
  readonly checkoutId: CheckoutId;
79
79
  }): ExecutionStep | undefined;
80
+ export type GitBranchStatus = Readonly<{
81
+ kind: 'complete';
82
+ /** An object name, or `(initial)` while HEAD is unborn. */
83
+ revision: string;
84
+ /** The branch name, or `(detached)` on a detached HEAD. */
85
+ branch: string;
86
+ upstream: string | null;
87
+ ahead: number;
88
+ behind: number;
89
+ /** False when Git names an upstream it cannot count against, because the upstream ref is gone. */
90
+ adjacencyReported: boolean;
91
+ trackedDirtyCount: number;
92
+ untrackedCount: number;
93
+ }>;
94
+ /** `git status --porcelain=v2 --branch` output as numbers and names, or why it cannot be read as such. */
95
+ export declare function parseGitBranchStatus(source: string): GitBranchStatus | Readonly<{
96
+ kind: 'unavailable';
97
+ reason: string;
98
+ }>;
80
99
  export declare function decodeGitObservation(bytes: Buffer, context: {
81
100
  readonly stackId: StackId;
82
101
  readonly observedAt: string;
@@ -111,20 +130,20 @@ export type ComparisonRemoteResult = Readonly<{
111
130
  reason: string;
112
131
  remoteName?: string;
113
132
  }>;
114
- export type ComparisonRemoteRefStage = Readonly<{
133
+ type ComparisonRemoteRefStage = Readonly<{
115
134
  kind: 'probe';
116
135
  step: ExecutionStep;
117
136
  }> | Extract<ComparisonRemoteResult, {
118
137
  kind: 'not-applicable' | 'unavailable';
119
138
  }>;
120
- export type ComparisonRemoteUrlStage = Readonly<{
139
+ type ComparisonRemoteUrlStage = Readonly<{
121
140
  kind: 'probe';
122
141
  step: ExecutionStep;
123
142
  remoteName: string;
124
143
  }> | Extract<ComparisonRemoteResult, {
125
144
  kind: 'unavailable';
126
145
  }>;
127
- export type ComparisonRemoteRevalidationStage = Readonly<{
146
+ type ComparisonRemoteRevalidationStage = Readonly<{
128
147
  kind: 'probe';
129
148
  step: ExecutionStep;
130
149
  remoteName: string;
@@ -147,7 +166,7 @@ export declare function createComparisonRemoteRefStep(input: {
147
166
  export declare function observeComparisonRemoteRef(stage: ComparisonRemoteRefStage, outcome: ExecuteProbeOutcome): ComparisonRemoteUrlStage;
148
167
  export declare function observeComparisonRemoteUrl(stage: ComparisonRemoteUrlStage, outcome: ExecuteProbeOutcome): ComparisonRemoteRevalidationStage;
149
168
  export declare function observeComparisonRemoteRevalidation(stage: ComparisonRemoteRevalidationStage, outcome: ExecuteProbeOutcome): ComparisonRemoteResult;
150
- export type RunningCheckoutObservationResult = {
169
+ type RunningCheckoutObservationResult = {
151
170
  readonly kind: 'complete';
152
171
  readonly observation: EnvironmentObservation;
153
172
  } | {
@@ -70,22 +70,8 @@ export function createGitObservationStep(input) {
70
70
  function unavailable(reason) {
71
71
  return { kind: 'unavailable', reason };
72
72
  }
73
- export function decodeGitObservation(bytes, context) {
74
- if (bytes.byteLength > OUTPUT_LIMIT || !/^stack:[a-f0-9]{64}$/.test(context.stackId)
75
- || !isCanonicalTimestamp(context.observedAt) || typeof context.source !== 'string'
76
- || context.source.length === 0 || Buffer.byteLength(`${context.source}:working-tree`) > 1_024
77
- || Buffer.byteLength(`${context.source}:adjacency`) > 1_024 || /[\x00-\x1f\x7f]/.test(context.source)
78
- || typeof context.checkoutRoot !== 'string' || context.checkoutRoot.length === 0 || /[\x00-\x1f\x7f]/.test(context.checkoutRoot)) {
79
- return unavailable('Git evidence exceeds its declared boundary');
80
- }
81
- let source;
82
- try {
83
- source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
84
- }
85
- catch {
86
- // Invalid UTF-8 is the only throw and is what the message names.
87
- return unavailable('Git evidence is not valid UTF-8');
88
- }
73
+ /** `git status --porcelain=v2 --branch` output as numbers and names, or why it cannot be read as such. */
74
+ export function parseGitBranchStatus(source) {
89
75
  let revision;
90
76
  let branch;
91
77
  let upstream = null;
@@ -94,61 +80,84 @@ export function decodeGitObservation(bytes, context) {
94
80
  let trackedDirtyCount = 0;
95
81
  let untrackedCount = 0;
96
82
  const headers = new Set();
83
+ const fail = (reason) => ({ kind: 'unavailable', reason });
97
84
  for (const line of source.split(/\r?\n/)) {
98
85
  if (line === '')
99
86
  continue;
100
87
  if (line.startsWith('# branch.oid ')) {
101
88
  if (headers.has('oid'))
102
- return unavailable('Git evidence contains a duplicate branch header');
89
+ return fail('Git evidence contains a duplicate branch header');
103
90
  headers.add('oid');
104
91
  revision = line.slice(13);
105
92
  }
106
93
  else if (line.startsWith('# branch.head ')) {
107
94
  if (headers.has('head'))
108
- return unavailable('Git evidence contains a duplicate branch header');
95
+ return fail('Git evidence contains a duplicate branch header');
109
96
  headers.add('head');
110
97
  branch = line.slice(14);
111
98
  }
112
99
  else if (line.startsWith('# branch.upstream ')) {
113
100
  if (headers.has('upstream'))
114
- return unavailable('Git evidence contains a duplicate branch header');
101
+ return fail('Git evidence contains a duplicate branch header');
115
102
  headers.add('upstream');
116
103
  upstream = line.slice(18);
117
104
  }
118
105
  else if (line.startsWith('# branch.ab ')) {
119
106
  if (headers.has('adjacency'))
120
- return unavailable('Git evidence contains a duplicate branch header');
107
+ return fail('Git evidence contains a duplicate branch header');
121
108
  headers.add('adjacency');
122
109
  const match = /^# branch\.ab \+(\d+) -(\d+)$/.exec(line);
123
110
  if (!match)
124
- return unavailable('Git adjacency evidence is malformed');
111
+ return fail('Git adjacency evidence is malformed');
125
112
  ahead = Number(match[1]);
126
113
  behind = Number(match[2]);
127
114
  if (!Number.isSafeInteger(ahead) || !Number.isSafeInteger(behind))
128
- return unavailable('Git adjacency evidence exceeds exact integer bounds');
115
+ return fail('Git adjacency evidence exceeds exact integer bounds');
129
116
  }
130
117
  else if (line.startsWith('? '))
131
118
  untrackedCount += 1;
132
119
  else if (/^[12u] /.test(line))
133
120
  trackedDirtyCount += 1;
134
121
  else if (!line.startsWith('! '))
135
- return unavailable('Git status evidence contains an unknown record');
122
+ return fail('Git status evidence contains an unknown record');
136
123
  }
137
124
  const validIdentity = (value) => Buffer.byteLength(value) <= 1_024 && !/[\x00-\x1f\x7f]/.test(value);
138
- // `git init` leaves HEAD unborn, and Git reports that as `(initial)` rather than an object name.
139
- const unborn = revision === '(initial)';
140
- if (!revision || !branch || !unborn && !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(revision) || !validIdentity(branch)
125
+ if (!revision || !branch || revision !== '(initial)' && !/^[a-f0-9]{40}(?:[a-f0-9]{24})?$/.test(revision) || !validIdentity(branch)
141
126
  || upstream !== null && !validIdentity(upstream)
142
127
  || upstream === null && headers.has('adjacency')) {
143
- return unavailable('Git evidence omitted or malformed repository identity');
128
+ return fail('Git evidence omitted or malformed repository identity');
144
129
  }
130
+ return { kind: 'complete', revision, branch, upstream, ahead, behind, adjacencyReported: headers.has('adjacency'),
131
+ trackedDirtyCount, untrackedCount };
132
+ }
133
+ export function decodeGitObservation(bytes, context) {
134
+ if (bytes.byteLength > OUTPUT_LIMIT || !/^stack:[a-f0-9]{64}$/.test(context.stackId)
135
+ || !isCanonicalTimestamp(context.observedAt) || typeof context.source !== 'string'
136
+ || context.source.length === 0 || Buffer.byteLength(`${context.source}:working-tree`) > 1_024
137
+ || Buffer.byteLength(`${context.source}:adjacency`) > 1_024 || /[\x00-\x1f\x7f]/.test(context.source)
138
+ || typeof context.checkoutRoot !== 'string' || context.checkoutRoot.length === 0 || /[\x00-\x1f\x7f]/.test(context.checkoutRoot)) {
139
+ return unavailable('Git evidence exceeds its declared boundary');
140
+ }
141
+ let source;
142
+ try {
143
+ source = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
144
+ }
145
+ catch {
146
+ // Invalid UTF-8 is the only throw and is what the message names.
147
+ return unavailable('Git evidence is not valid UTF-8');
148
+ }
149
+ const parsed = parseGitBranchStatus(source);
150
+ if (parsed.kind !== 'complete')
151
+ return unavailable(parsed.reason);
152
+ const { revision, branch, upstream, ahead, behind, trackedDirtyCount, untrackedCount, adjacencyReported } = parsed;
153
+ const unborn = revision === '(initial)';
145
154
  const committed = unborn ? null : revision;
146
155
  const workingTree = trackedDirtyCount > 0 && untrackedCount > 0 ? 'dirty-and-untracked'
147
156
  : trackedDirtyCount > 0 ? 'dirty' : untrackedCount > 0 ? 'untracked' : 'clean';
148
157
  // Git names an upstream without counting against it when the upstream ref no longer exists,
149
158
  // which is what a merged and deleted remote branch leaves behind.
150
159
  const adjacency = upstream === null ? 'untracked-upstream'
151
- : !headers.has('adjacency') ? 'upstream-gone'
160
+ : !adjacencyReported ? 'upstream-gone'
152
161
  : ahead > 0 && behind > 0 ? 'diverged'
153
162
  : ahead > 0 ? 'ahead' : behind > 0 ? 'behind' : 'aligned';
154
163
  const evidence = { revision: committed, branch, upstream, ahead, behind, trackedDirtyCount, untrackedCount, workingTree, adjacency };
@@ -2,10 +2,11 @@ import type { ExecuteProbeOutcome } from '../environment-exec/execute.js';
2
2
  import { type DoctorCommandAdapters } from './application/doctor.js';
3
3
  import { type PreflightCommandAdapters } from './application/health.js';
4
4
  import { type ExecutionStep } from './contract.js';
5
- export interface HealthCommandCompositionInput {
5
+ interface HealthCommandCompositionInput {
6
6
  readonly configPath: string;
7
7
  readonly env: Readonly<Record<string, string | undefined>>;
8
8
  readonly timeoutMs: number;
9
9
  }
10
- export type HealthCommandAdapterComposition = PreflightCommandAdapters & DoctorCommandAdapters;
10
+ type HealthCommandAdapterComposition = PreflightCommandAdapters & DoctorCommandAdapters;
11
11
  export declare function createHealthCommandAdapters(rawInput: HealthCommandCompositionInput, execute: (step: ExecutionStep) => Promise<ExecuteProbeOutcome>): HealthCommandAdapterComposition;
12
+ export {};
@@ -14,6 +14,7 @@ import { observeEnvSchemas } from './env-schema-observation.js';
14
14
  import { hasErrorCode, RefusalError, ENVIRONMENT_CODES, errorCause, } from './contract.js';
15
15
  import { declaresNothingToRun, runtimeNotDeclared } from './declaration.js';
16
16
  import { resolveDockerExecutable, resolveGitExecutable } from './executable.js';
17
+ import { inspectMachineStacksForDoctor } from './stack-inventory.js';
17
18
  import { createGitObservationStep, observeGit, observeRunningCheckout } from './git-observation.js';
18
19
  import { inspectProjectReadiness } from './readiness-composition.js';
19
20
  import { stackId } from './identity.js';
@@ -30,6 +31,8 @@ import { readLocalRuntimeMeasurements } from './runtime-measurement-record.js';
30
31
  import { consumeSetupCheckoutProjection, observeSetupCheckout, withSetupCheckoutProjection, } from './setup-observation.js';
31
32
  import { observeConfiguration, readState, selectStateRoot } from './state.js';
32
33
  import { observeTemplateHealth } from './templates.js';
34
+ import { inspectServedSiblings } from './application/siblings.js';
35
+ import { observeServedCheckouts } from './sibling-observation.js';
33
36
  function captureInput(value) {
34
37
  try {
35
38
  if (!value || typeof value !== 'object' || Array.isArray(value))
@@ -347,6 +350,10 @@ function producers(input) {
347
350
  state: async () => observeConfiguration({
348
351
  path: input.config.result.source.path, stackId: input.stack, observedAt: observedAt(),
349
352
  }),
353
+ siblings: async () => observeServedCheckouts({
354
+ stackId: input.stack, observedAt: observedAt(), configPath: input.config.result.source.path,
355
+ config: input.config.result.config,
356
+ }),
350
357
  templates: async () => observeTemplateHealth({
351
358
  stackId: input.stack, observedAt: observedAt(), siblingRoot,
352
359
  siblings: input.config.result.config.sibling,
@@ -414,6 +421,8 @@ async function inspectWorkEvaluator(root, env, evaluators) {
414
421
  export function createHealthCommandAdapters(rawInput, execute) {
415
422
  const input = captureInput(rawInput);
416
423
  let selectedNamespace;
424
+ let loadedConfig;
425
+ let selectedOwn;
417
426
  const load = async (command, issue, refuse, purpose) => {
418
427
  const requestedPath = commandConfigPath(command);
419
428
  if (!input || typeof execute !== 'function' || typeof issue !== 'function'
@@ -425,6 +434,7 @@ export function createHealthCommandAdapters(rawInput, execute) {
425
434
  return refuse?.(loaded.diagnostics);
426
435
  if (loaded.kind !== 'complete')
427
436
  return undefined;
437
+ loadedConfig = loaded;
428
438
  // Health is about a running stack or a machine floor; with neither declared it is not declared,
429
439
  // never healthy, and preflight refuses. Doctor still checks what such a cockpit does depend on:
430
440
  // its Git working tree, its siblings and its surfaces.
@@ -460,6 +470,8 @@ export function createHealthCommandAdapters(rawInput, execute) {
460
470
  checkoutId: checkout.checkoutId,
461
471
  variant: { kind: 'shared' },
462
472
  });
473
+ selectedOwn = { cockpitId: checkout.cockpitId, stackId: currentStack, mainCheckoutPath: checkout.mainCheckoutPath,
474
+ configPath: loaded.result.source.path };
463
475
  let stopped = notDeclared ?? null;
464
476
  const refreshPreflight = async (previous) => {
465
477
  const observedAt = new Date().toISOString();
@@ -622,7 +634,12 @@ export function createHealthCommandAdapters(rawInput, execute) {
622
634
  const inventory = await readRootPortInventory(selectedNamespace);
623
635
  return inventory.kind === 'complete' ? { kind: 'complete', skipped: inventory.skipped } : inventory;
624
636
  };
625
- return { load, inspectSurfaceOverrides, ...(inspectHookTrust ? { inspectHookTrust } : {}),
626
- ...(inspectEvaluator ? { inspectEvaluator } : {}), ...(inspectReadiness ? { inspectReadiness } : {}), readUnreadableState,
637
+ const inspectStacks = async () => selectedNamespace && selectedOwn ? inspectMachineStacksForDoctor({ namespace: selectedNamespace, own: selectedOwn, configPath: selectedOwn.configPath }) : [];
638
+ const inspectServed = input ? (loaded) => {
639
+ const reuse = loaded ?? loadedConfig;
640
+ return inspectServedSiblings({ configPath: input.configPath, env: input.env, ...(reuse ? { loaded: reuse } : {}) }, { execute });
641
+ } : undefined;
642
+ return { load, inspectSurfaceOverrides, ...(inspectHookTrust ? { inspectHookTrust } : {}), ...(inspectServed ? { inspectServedSiblings: inspectServed } : {}),
643
+ ...(inspectEvaluator ? { inspectEvaluator } : {}), ...(inspectReadiness ? { inspectReadiness } : {}), readUnreadableState, inspectStacks,
627
644
  execute: (step) => execute(step) };
628
645
  }
@@ -1,6 +1,6 @@
1
1
  import type { Diagnostic } from '../diagnostics.js';
2
2
  import { type ReadinessReport } from '../readiness.js';
3
- import { type AnswerProvenance, type DocumentCommandInput, type DocumentOutcome, type EstateClassificationOutcome, type EstateClassificationResult, type UnconfirmedAnswer, type EstateObservationInput } from '../init/draft.js';
3
+ import { type AnswerProvenance, type DocumentCommandInput, type DocumentOutcome, type EstateClassificationOutcome, type EstateClassificationResult, type DerivedAnswer, type UnconfirmedAnswer, type EstateObservationInput } from '../init/draft.js';
4
4
  import { type SurfaceTarget } from '../surfaces/contract.js';
5
5
  import { type BootstrapStateAddress, type BootstrapStateRecord, type SelectBootstrapStateRootInput, type StateResult } from './state.js';
6
6
  export interface InitApplicationInput {
@@ -37,8 +37,8 @@ export interface InitApplicationInput {
37
37
  firstSibling: string;
38
38
  }>;
39
39
  }
40
- export type CockpitRootAnswer = 'dedicated' | 'in-repo';
41
- export interface CockpitRootOption {
40
+ type CockpitRootAnswer = 'dedicated' | 'in-repo';
41
+ interface CockpitRootOption {
42
42
  readonly answer: CockpitRootAnswer;
43
43
  readonly root: string;
44
44
  /** Directories init creates before it writes anything else; each is outside the directory init ran in. */
@@ -125,7 +125,7 @@ type ResumedBootstrapOutcome = Readonly<{
125
125
  diagnostics: readonly Diagnostic[];
126
126
  result: null;
127
127
  }>;
128
- export type DestinationObservation = Readonly<{
128
+ type DestinationObservation = Readonly<{
129
129
  kind: 'absent';
130
130
  }> | Readonly<{
131
131
  kind: 'available';
@@ -174,7 +174,7 @@ export interface InitApplicationAdapters {
174
174
  readonly observeRoot?: (input: Readonly<{
175
175
  root: string;
176
176
  }>) => Promise<'product-repository' | 'cockpit-root'>;
177
- /** Proposes answers from files the repositories already carry; every value comes back as inferred. */
177
+ /** Proposes answers from files the repositories already carry, each derived (exact and complete) or inferred. */
178
178
  readonly infer?: (input: Readonly<{
179
179
  root: string;
180
180
  configurationRoot?: string;
@@ -188,7 +188,7 @@ export interface InitApplicationAdapters {
188
188
  target: string;
189
189
  value: unknown;
190
190
  provenance: Readonly<{
191
- kind: 'inferred';
191
+ kind: 'derived' | 'inferred';
192
192
  source: string;
193
193
  reason: string;
194
194
  }>;
@@ -198,6 +198,10 @@ export interface InitApplicationAdapters {
198
198
  reason: string;
199
199
  }>[];
200
200
  }>>;
201
+ /** What the container runtime on this machine offers, read when the interview will ask for the machine floor. */
202
+ readonly measureMachine?: (input: Readonly<{
203
+ root: string;
204
+ }>) => Promise<MachineMeasurement>;
201
205
  /** What check would report about the Compose files a drafted cockpit.toml declares; init never edits them, so it shows the edit. */
202
206
  readonly composeFindings?: (input: Readonly<{
203
207
  root: string;
@@ -222,7 +226,7 @@ export interface InitApplicationAdapters {
222
226
  findings: readonly Diagnostic[];
223
227
  }>>;
224
228
  }
225
- export interface GreenfieldQuestion {
229
+ interface GreenfieldQuestion {
226
230
  readonly id: string;
227
231
  readonly field: string;
228
232
  readonly source: string;
@@ -231,7 +235,7 @@ export interface GreenfieldQuestion {
231
235
  /** Lines preserve the owner draft exactly when joined with a newline, within bounded human rendering. */
232
236
  readonly document?: readonly string[];
233
237
  }
234
- export interface GreenfieldResult extends EstateClassificationResult {
238
+ interface GreenfieldResult extends EstateClassificationResult {
235
239
  readonly sourceObservationSha256: string;
236
240
  readonly answers: readonly Readonly<{
237
241
  questionId: string;
@@ -240,23 +244,42 @@ export interface GreenfieldResult extends EstateClassificationResult {
240
244
  provenance: AnswerProvenance;
241
245
  }>[];
242
246
  }
243
- /** One proposed value for an unanswered field. It stays a proposal until the operator confirms it in an answer. */
244
- export interface InferredItem {
247
+ type MachineMeasurement = Readonly<{
248
+ kind: 'complete';
249
+ memoryMb: number;
250
+ cpus: number;
251
+ observedAt: string;
252
+ }> | Readonly<{
253
+ kind: 'unavailable';
254
+ reason: string;
255
+ }>;
256
+ /**
257
+ * One proposed value for an unanswered field, and how it was established: "derived" is the complete
258
+ * answer read exactly from the named file and line, "measured" was observed on this machine, and
259
+ * "inferred" is a hint or partial value that needs judgement.
260
+ */
261
+ interface Proposal {
245
262
  readonly target: string;
246
263
  readonly value: unknown;
247
264
  readonly provenance: Readonly<{
248
- kind: 'inferred';
265
+ kind: 'derived' | 'measured' | 'inferred';
249
266
  source: string;
250
267
  reason: string;
251
268
  }>;
252
269
  }
253
- export interface OutstandingField {
270
+ interface OutstandingField {
254
271
  readonly field: string;
255
272
  readonly why: string;
256
- readonly inferred: readonly InferredItem[];
273
+ /**
274
+ * "agent" when a derived proposal answers the whole field: the setup agent may accept it with
275
+ * provenance "derived", and bring-up proves it. Everything else needs a person: security
276
+ * boundaries, and values that are inferred, measured or chosen.
277
+ */
278
+ readonly confirmedBy: 'agent' | 'person';
279
+ readonly proposals: readonly Proposal[];
257
280
  }
258
281
  /** Every field the interview will still ask, in order, so an unattended caller can answer them in one pass. */
259
- export interface InterviewPlan {
282
+ interface InterviewPlan {
260
283
  /** Fields an earlier run already answered; their answers are kept. */
261
284
  readonly answered: readonly string[];
262
285
  readonly outstanding: readonly OutstandingField[];
@@ -286,6 +309,8 @@ export interface InitResult {
286
309
  }>;
287
310
  /** Answers in cockpit.toml no person stated; each stays listed there, and doctor warns, until someone confirms it. */
288
311
  readonly unconfirmed?: readonly UnconfirmedAnswer[];
312
+ /** Answers the setup agent accepted as read from exact sources, each with the file and line it came from. */
313
+ readonly derived?: readonly DerivedAnswer[];
289
314
  /** This run's findings grouped under the prerequisites cockpit depends on: what to do next. */
290
315
  readonly readiness?: ReadinessReport;
291
316
  }