@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.
- package/CHANGELOG.md +99 -0
- package/README.md +3 -0
- package/cockpit.schema.json +73 -3
- package/dist/autonomy.d.ts +3 -2
- package/dist/autonomy.js +30 -13
- package/dist/browser/commands.d.ts +57 -2
- package/dist/browser/commands.js +114 -10
- package/dist/browser/runtime.d.ts +1 -0
- package/dist/browser/runtime.js +4 -1
- package/dist/browser/surfaces.d.ts +7 -7
- package/dist/browser/surfaces.js +5 -0
- package/dist/canonical-json.d.ts +2 -0
- package/dist/canonical-json.js +5 -0
- package/dist/cli-main.d.ts +40 -3
- package/dist/cli-main.js +241 -62
- package/dist/command-table.d.ts +19 -11
- package/dist/command-table.js +190 -38
- package/dist/config/load.d.ts +16 -4
- package/dist/config/load.js +25 -3
- package/dist/config/unconfirmed-toml.d.ts +9 -0
- package/dist/config/unconfirmed-toml.js +29 -0
- package/dist/diagnostics.js +10 -1
- package/dist/docs/check.d.ts +16 -0
- package/dist/docs/check.js +197 -0
- package/dist/docs/decision-record.d.ts +34 -0
- package/dist/docs/decision-record.js +135 -0
- package/dist/docs/decisions.d.ts +16 -0
- package/dist/docs/decisions.js +103 -0
- package/dist/docs/markdown.d.ts +36 -0
- package/dist/docs/markdown.js +88 -0
- package/dist/docs/references.d.ts +32 -0
- package/dist/docs/references.js +148 -0
- package/dist/environment/application/confirm.d.ts +35 -0
- package/dist/environment/application/confirm.js +122 -0
- package/dist/environment/application/destructive.js +6 -2
- package/dist/environment/application/doctor.d.ts +12 -7
- package/dist/environment/application/doctor.js +20 -8
- package/dist/environment/application/guard.d.ts +12 -6
- package/dist/environment/application/guard.js +87 -32
- package/dist/environment/application/health.d.ts +14 -5
- package/dist/environment/application/health.js +8 -2
- package/dist/environment/application/lifecycle-context.d.ts +10 -3
- package/dist/environment/application/lifecycle-state.js +4 -3
- package/dist/environment/application/lifecycle.d.ts +2 -1
- package/dist/environment/application/lifecycle.js +11 -3
- package/dist/environment/application/services.d.ts +60 -0
- package/dist/environment/application/services.js +89 -0
- package/dist/environment/application/siblings.d.ts +43 -0
- package/dist/environment/application/siblings.js +269 -0
- package/dist/environment/claims.d.ts +10 -10
- package/dist/environment/claims.js +3 -11
- package/dist/environment/compose-topology.d.ts +15 -7
- package/dist/environment/compose-topology.js +100 -23
- package/dist/environment/contract.d.ts +42 -9
- package/dist/environment/contract.js +33 -0
- package/dist/environment/executable.d.ts +12 -0
- package/dist/environment/executable.js +46 -23
- package/dist/environment/generated-application/check.js +2 -0
- package/dist/environment/generated-application/prepare.js +2 -0
- package/dist/environment/generated-application/shared.d.ts +10 -2
- package/dist/environment/generated-application/shared.js +6 -1
- package/dist/environment/generated-application/surface-outputs.js +23 -12
- package/dist/environment/generated-application/surfaces.js +16 -11
- package/dist/environment/generated-application/sync.js +19 -4
- package/dist/environment/git-observation.d.ts +25 -6
- package/dist/environment/git-observation.js +37 -28
- package/dist/environment/health-composition.d.ts +3 -2
- package/dist/environment/health-composition.js +19 -2
- package/dist/environment/init-application.d.ts +39 -14
- package/dist/environment/init-application.js +39 -10
- package/dist/environment/lifecycle-composition.d.ts +4 -2
- package/dist/environment/lifecycle-composition.js +84 -17
- package/dist/environment/lifecycle-stage.d.ts +8 -3
- package/dist/environment/lifecycle-stage.js +11 -1
- package/dist/environment/logs.d.ts +1 -1
- package/dist/environment/map-limit.d.ts +2 -0
- package/dist/environment/map-limit.js +13 -0
- package/dist/environment/mcp-observation.d.ts +9 -0
- package/dist/environment/mcp-observation.js +46 -0
- package/dist/environment/observations.d.ts +1 -1
- package/dist/environment/observations.js +2 -0
- package/dist/environment/operator-application.d.ts +35 -6
- package/dist/environment/operator-application.js +42 -0
- package/dist/environment/presence.d.ts +4 -0
- package/dist/environment/presence.js +26 -0
- package/dist/environment/read-composition.d.ts +8 -3
- package/dist/environment/read-composition.js +53 -8
- package/dist/environment/readiness-composition.d.ts +20 -1
- package/dist/environment/readiness-composition.js +23 -2
- package/dist/environment/reclaim.d.ts +56 -19
- package/dist/environment/reclaim.js +147 -42
- package/dist/environment/runtime.d.ts +64 -18
- package/dist/environment/runtime.js +73 -3
- package/dist/environment/served-checkout-grants.d.ts +53 -0
- package/dist/environment/served-checkout-grants.js +182 -0
- package/dist/environment/sibling-checkouts.d.ts +119 -0
- package/dist/environment/sibling-checkouts.js +254 -0
- package/dist/environment/sibling-observation.d.ts +27 -0
- package/dist/environment/sibling-observation.js +45 -0
- package/dist/environment/sibling-pull.d.ts +23 -0
- package/dist/environment/sibling-pull.js +95 -0
- package/dist/environment/stack-inputs.d.ts +18 -0
- package/dist/environment/stack-inputs.js +210 -0
- package/dist/environment/stack-inventory-prune.d.ts +31 -0
- package/dist/environment/stack-inventory-prune.js +129 -0
- package/dist/environment/stack-inventory-report.d.ts +44 -0
- package/dist/environment/stack-inventory-report.js +78 -0
- package/dist/environment/stack-inventory.d.ts +94 -0
- package/dist/environment/stack-inventory.js +418 -0
- package/dist/environment/state.d.ts +48 -9
- package/dist/environment/state.js +142 -2
- package/dist/environment/tool-registration.js +3 -7
- package/dist/environment-exec/execute.d.ts +6 -3
- package/dist/environment-exec/execute.js +4 -0
- package/dist/fix-catalogue.d.ts +1 -1
- package/dist/generated-environment/adopt.js +4 -8
- package/dist/guards/answer-confirmation.d.ts +41 -0
- package/dist/guards/answer-confirmation.js +109 -0
- package/dist/guards/evaluator.d.ts +20 -3
- package/dist/guards/evaluator.js +39 -7
- package/dist/guards/local-identity.d.ts +6 -2
- package/dist/guards/local-identity.js +10 -4
- package/dist/guards/policy-change.d.ts +43 -0
- package/dist/guards/policy-change.js +159 -0
- package/dist/guards/policy-pin.d.ts +1 -1
- package/dist/guards/policy-pin.js +14 -12
- package/dist/init/command.d.ts +4 -3
- package/dist/init/command.js +36 -16
- package/dist/init/derive.d.ts +8 -6
- package/dist/init/derive.js +140 -36
- package/dist/init/draft.d.ts +13 -11
- package/dist/init/draft.js +54 -31
- package/dist/init/render.d.ts +18 -6
- package/dist/init/render.js +31 -9
- package/dist/repository-path.d.ts +9 -0
- package/dist/repository-path.js +16 -0
- package/dist/schema.d.ts +42 -75
- package/dist/schema.js +62 -11
- package/dist/surfaces/harness-registry.d.ts +80 -0
- package/dist/surfaces/harness-registry.js +29 -0
- package/dist/surfaces/mcp.d.ts +67 -17
- package/dist/surfaces/mcp.js +194 -31
- package/dist/surfaces/projection.d.ts +7 -7
- package/dist/surfaces/projection.js +41 -6
- package/dist/surfaces/render.d.ts +5 -44
- package/dist/surfaces/render.js +5 -2
- package/dist/team-metadata.js +2 -1
- package/dist/testing/test-protection.d.ts +34 -0
- package/dist/testing/test-protection.js +137 -0
- package/dist/testing/weakening.d.ts +15 -0
- package/dist/testing/weakening.js +30 -0
- package/dist/work-orders/application-closure.d.ts +7 -0
- package/dist/work-orders/application-closure.js +76 -0
- package/dist/work-orders/application-context.d.ts +162 -0
- package/dist/work-orders/application-context.js +137 -0
- package/dist/work-orders/application-decisions.d.ts +27 -0
- package/dist/work-orders/application-decisions.js +121 -0
- package/dist/work-orders/application-grading.d.ts +11 -0
- package/dist/work-orders/application-grading.js +212 -0
- package/dist/work-orders/application.d.ts +4 -97
- package/dist/work-orders/application.js +94 -254
- package/dist/work-orders/composition.d.ts +35 -58
- package/dist/work-orders/composition.js +216 -24
- package/dist/work-orders/contract.d.ts +22 -6
- package/dist/work-orders/contract.js +34 -16
- package/dist/work-orders/decisions.d.ts +23 -0
- package/dist/work-orders/decisions.js +55 -0
- package/dist/work-orders/store.d.ts +54 -5
- package/dist/work-orders/store.js +118 -24
- package/docs/README.md +6 -3
- package/docs/capability-matrix.md +17 -9
- package/docs/cli-reference.md +209 -1
- package/docs/command-list.md +12 -1
- package/docs/concepts.md +6 -4
- package/docs/configuration-essentials.md +7 -2
- package/docs/configuration-reference.md +18 -3
- package/docs/docs-check.md +76 -0
- package/docs/environment.md +194 -6
- package/docs/faq.md +5 -3
- package/docs/getting-started.md +109 -25
- package/docs/harness-surfaces.md +69 -2
- package/docs/safety-and-autonomy.md +56 -12
- package/docs/troubleshooting.md +29 -8
- package/docs/tutorial.md +3 -2
- package/docs/work-and-browser.md +180 -14
- package/libexec/browser-operation.cjs +86 -2
- package/package.json +2 -2
- package/surfaces/skills/cockpit-browser.skill.md +7 -2
- package/surfaces/skills/cockpit-explain.skill.md +1 -1
- package/surfaces/skills/cockpit-fix.skill.md +1 -1
- package/surfaces/skills/cockpit-setup.skill.md +3 -3
- package/surfaces/skills/cockpit-work.skill.md +1 -1
- package/surfaces/skills/procedures/explain.md +9 -4
- package/surfaces/skills/procedures/fix.md +25 -11
- package/surfaces/skills/procedures/setup.md +113 -40
- 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`)
|
|
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 *', '
|
|
236
|
-
'
|
|
237
|
-
'work
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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 =
|
|
261
|
-
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
: !
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
626
|
-
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
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
|
-
|
|
270
|
+
interface OutstandingField {
|
|
254
271
|
readonly field: string;
|
|
255
272
|
readonly why: string;
|
|
256
|
-
|
|
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
|
-
|
|
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
|
}
|