@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
@@ -1,10 +1,10 @@
1
1
  import type { ParseArgsConfig } from 'node:util';
2
- import type { GuardInvocationProfile } from './guards/evaluator.js';
2
+ import { type GuardInvocationProfile } from './guards/evaluator.js';
3
3
  import { GUARD_HANDLER_ARGV } from './guards/harness-adapter.js';
4
4
  export declare function isRecordedAnswerField(value: string): boolean;
5
- export type WorkHandlerIdentifier = 'work-status' | 'work-accept' | 'work-check' | 'work-note' | 'work-answer' | 'work-grade' | 'work-signoff' | 'work-recover' | 'work-verify' | 'grade-criteria' | 'browser-drive' | 'browser-screenshot' | 'test-run';
6
- export type HandlerIdentifier = WorkHandlerIdentifier | 'check' | 'doctor' | 'down' | 'init' | 'logs' | 'preflight' | 'registry-resolve' | 'reset' | 'runtime' | 'setup' | 'selftest' | 'sibling' | 'tool' | 'session' | 'sleep' | 'sync' | 'up' | 'upgrade';
7
- export type CommandStage = 'set-up' | 'generate-and-verify' | 'run-the-stack' | 'diagnose' | 'test-and-grade';
5
+ export type WorkHandlerIdentifier = 'work-status' | 'work-accept' | 'work-check' | 'work-note' | 'work-decide' | 'work-answer' | 'work-acknowledge' | 'work-grade' | 'work-signoff' | 'work-recover' | 'work-verify' | 'grade-criteria' | 'browser-drive' | 'browser-screenshot' | 'test-run';
6
+ export type HandlerIdentifier = WorkHandlerIdentifier | 'check' | 'confirm' | 'docs-check' | 'doctor' | 'down' | 'exec' | 'init' | 'logs' | 'preflight' | 'ps' | 'rebuild' | 'registry-resolve' | 'reset' | 'restart' | 'runtime' | 'setup' | 'selftest' | 'sibling' | 'tool' | 'session' | 'sleep' | 'stacks' | 'sync' | 'up' | 'upgrade' | 'use' | 'pull';
7
+ type CommandStage = 'set-up' | 'generate-and-verify' | 'run-the-stack' | 'diagnose' | 'test-and-grade';
8
8
  /** The order a newcomer meets the commands in, as the README's "Commands at a glance" table names it. */
9
9
  export declare const commandStages: readonly Readonly<{
10
10
  id: CommandStage;
@@ -41,14 +41,14 @@ export interface CommandDefinition {
41
41
  ordinary: OrdinaryCommandMetadata;
42
42
  }
43
43
  export declare function describePublicOption(name: string): string;
44
- type OrdinaryValueRole = 'confined-path' | 'resolved-config' | 'declared-service' | 'declared-gate' | 'gate-mode' | 'gate-selector' | 'identifier' | 'label' | 'field' | 'tail' | 'since' | 'sha256' | 'surface-target' | 'local-image' | 'evaluator-tool';
45
- export interface OrdinaryOption {
44
+ type OrdinaryValueRole = 'confined-path' | 'resolved-config' | 'declared-service' | 'declared-gate' | 'gate-mode' | 'gate-selector' | 'identifier' | 'route' | 'label' | 'field' | 'tail' | 'since' | 'sha256' | 'surface-target' | 'local-image' | 'evaluator-tool' | 'compose-profile';
45
+ interface OrdinaryOption {
46
46
  readonly kind: 'flag' | 'value';
47
47
  readonly role: 'boolean' | OrdinaryValueRole;
48
- readonly repetition: 'singleton';
48
+ readonly repetition: 'singleton' | 'repeated';
49
49
  readonly required?: true;
50
50
  }
51
- export type OrdinaryCommandMetadata = Readonly<{
51
+ type OrdinaryCommandMetadata = Readonly<{
52
52
  availability: 'unavailable';
53
53
  reason: string;
54
54
  }> | Readonly<{
@@ -58,7 +58,7 @@ export type OrdinaryCommandMetadata = Readonly<{
58
58
  positional: Readonly<{
59
59
  min: number;
60
60
  max: number;
61
- roles: readonly ['confined-root'] | readonly ['operation'] | readonly ['version'] | readonly [];
61
+ roles: readonly ['confined-root'] | readonly ['operation'] | readonly ['version'] | readonly ['slug', 'confined-root'] | readonly ['operation', 'surface-target'] | readonly [];
62
62
  operations?: readonly string[];
63
63
  }>;
64
64
  options: Readonly<Record<string, OrdinaryOption>>;
@@ -67,11 +67,12 @@ export type OrdinaryCommandMetadata = Readonly<{
67
67
  forbidden: Readonly<{
68
68
  presence: readonly string[];
69
69
  }>;
70
+ admittedWhileUnsynced?: true;
70
71
  requires?: OrdinaryGrant;
71
72
  }>;
72
73
  export declare function isWorkHandler(value: HandlerIdentifier): value is WorkHandlerIdentifier;
73
74
  export declare const commands: readonly CommandDefinition[];
74
- export interface InternalCommandDefinition {
75
+ interface InternalCommandDefinition {
75
76
  readonly name: 'guard-evaluate';
76
77
  readonly handler: 'guard-evaluate';
77
78
  readonly visibility: 'internal';
@@ -87,7 +88,7 @@ export interface InternalCommandDefinition {
87
88
  }>;
88
89
  }
89
90
  export declare const internalCommands: readonly InternalCommandDefinition[];
90
- export interface OrdinaryInvocationAuthority {
91
+ interface OrdinaryInvocationAuthority {
91
92
  readonly executable: string;
92
93
  readonly canonicalRoot: string;
93
94
  readonly allowedRoots: readonly string[];
@@ -97,8 +98,14 @@ export interface OrdinaryInvocationAuthority {
97
98
  readonly gates: readonly string[];
98
99
  /** What the configuration grants an agent: browser preapproval, and the [autonomy] decisions set to agent. */
99
100
  readonly grants: readonly OrdinaryGrant[];
101
+ /** Compose profiles an agent may name with --profile; any other starts services no person declared for it. */
102
+ readonly profiles: readonly string[];
100
103
  }
101
104
  export type OrdinaryGrant = 'browser-preapproval' | 'autonomy-sync' | 'autonomy-reset';
105
+ /** The commands the guard admits while a non-widening edit of cockpit.toml waits for sync. */
106
+ export declare const UNSYNCED_ADMITTED_COMMANDS: readonly string[];
107
+ /** The same commands as prose, for the hold that names them: "cockpit sync and check". */
108
+ export declare const UNSYNCED_ADMITTED_PROSE: string;
102
109
  /**
103
110
  * The inner command of an exact `session --config <configured file> -- <command…>` run from the
104
111
  * cockpit root, or undefined. The guard admits the session only when it admits the inner command,
@@ -107,6 +114,7 @@ export type OrdinaryGrant = 'browser-preapproval' | 'autonomy-sync' | 'autonomy-
107
114
  export declare function ordinarySessionCommand(argv: readonly string[], cwd: string, authority: Readonly<{
108
115
  canonicalRoot: string;
109
116
  configPath: string;
117
+ profiles: readonly string[];
110
118
  }>): readonly string[] | undefined;
111
119
  /** Classify one exact candidate; the caller authenticates filesystem and executable authority. */
112
120
  export declare function resolveOrdinaryInvocation(input: GuardInvocationProfile, authority: OrdinaryInvocationAuthority, inspectPath: (path: string) => boolean): GuardInvocationProfile | undefined;
@@ -2,6 +2,7 @@ import { SURFACE_TARGETS } from './surfaces/contract.js';
2
2
  import { isAbsolute, normalize, resolve } from 'node:path';
3
3
  import { WORK_ORDER_ID } from './work-orders/contract.js';
4
4
  import { snapshotJson } from './environment/contract.js';
5
+ import { innerCommand, WRAPPING_COMMANDS } from './guards/evaluator.js';
5
6
  import { GUARD_HANDLER_ARGV } from './guards/harness-adapter.js';
6
7
  import { GUARD_INPUT_LIMITS } from './guards/invocation-parser.js';
7
8
  export function isRecordedAnswerField(value) {
@@ -23,6 +24,7 @@ export const commandStages = Object.freeze([
23
24
  export const commandGroups = Object.freeze([
24
25
  { name: 'work', summary: 'Accept, check, grade and close work orders' },
25
26
  { name: 'browser', summary: 'Drive, inspect and capture the declared application in a browser' },
27
+ { name: 'docs', summary: 'Check living documents and decision records against the repositories' },
26
28
  ]);
27
29
  /**
28
30
  * The public command an argv names, and the arguments after it. A group word takes the next word
@@ -42,6 +44,7 @@ const FLAG_DESCRIPTIONS = Object.freeze({
42
44
  'accept-preview': 'Accept the displayed init preview with its exact SHA-256 digest',
43
45
  'accept-reset': 'Accept the displayed answer reset with its exact SHA-256 digest',
44
46
  abandon: 'Close the order as abandoned; --note says why',
47
+ clear: 'Serve the sibling from its declared checkout again',
45
48
  adopt: 'Move one existing harness settings path under cockpit ownership',
46
49
  adoption: 'Path to the legacy adoption shell data',
47
50
  answers: 'Path to the init answers file',
@@ -50,6 +53,7 @@ const FLAG_DESCRIPTIONS = Object.freeze({
50
53
  command: 'With --scaffold-compose: the command that starts the app, as words separated by spaces',
51
54
  config: 'Path to cockpit.toml',
52
55
  create: 'Create a new local sibling repository',
56
+ decision: 'With work note: record the text as a decision the order made, resolved with work decide before signoff. With work decide: number of the decision to resolve, as work status lists it',
53
57
  criteria: 'Path to a file whose YAML front-matter lists the criteria to grade',
54
58
  dir: 'Directory for the sibling checkout',
55
59
  evaluator: 'Grade with only this agent tool (codexcli, claudecode or opencode), with no fallback to another',
@@ -66,16 +70,24 @@ const FLAG_DESCRIPTIONS = Object.freeze({
66
70
  note: 'What was checked, or what was and was not done',
67
71
  partial: 'Close the order as partly done; --note says what is left',
68
72
  port: 'With --scaffold-compose: the port the app listens on inside its container',
73
+ profile: 'Compose profile to activate beside environment.profiles; repeat it for several',
69
74
  'probe-specifications': 'Path to the harness probe specifications; with --write-probe-specifications, the earlier file whose filled rows for unchanged cells carry forward',
70
75
  purpose: 'Short purpose of the sibling repository',
71
76
  question: 'Number of the open question to answer, as work status lists it',
72
77
  registry: 'Path to the legacy registry input',
78
+ reason: 'With --none: why the decision is not a lasting one',
79
+ record: 'Write a new decision record under [docs] decisions for the decision',
80
+ 'record-file': 'An existing decision record under [docs] decisions that cites the order',
81
+ supersedes: 'With --record: the number of the decision record the new one supersedes, which is marked superseded',
82
+ none: 'Resolve the decision as not lasting; --reason says why',
73
83
  'reset-answer': 'Preview clearing one recorded init answer field',
74
84
  remote: 'Repository remote URL recorded for the sibling',
75
85
  role: 'Declared browser role',
86
+ 'fail-on-findings': 'Exit 2 when a route has console errors, uncaught errors or failed requests, for a criterion that wants a clean route',
87
+ route: 'Declared browser route to visit, by name or path; every declared route when omitted',
76
88
  'scaffold-compose': 'Write a starting compose.yml for the app in this directory, relative to the root, once; it refuses when a Compose file exists',
77
89
  select: 'Optional subset of the gate to run',
78
- service: 'Declared service whose logs are collected',
90
+ service: 'Declared Compose service the command acts on',
79
91
  'siblings-root': 'Root directory containing sibling checkouts',
80
92
  since: 'Collect logs no older than this duration',
81
93
  tail: 'Maximum number of log records to collect',
@@ -97,6 +109,13 @@ export function describePublicOption(name) {
97
109
  }
98
110
  // Mirrors EvaluatorTool in schema.ts, which this data table may not import.
99
111
  const EVALUATOR_TOOLS = ['codexcli', 'claudecode', 'opencode'];
112
+ // Mirrors UNCONFIRMABLE_ANSWERS in schema.ts. Every answer is admitted, so confirm itself names the
113
+ // person an answer needs instead of the guard refusing it unexplained.
114
+ const UNCONFIRMED_ANSWER_WORDS = Object.freeze(['cockpit.name', 'sibling', 'estate.runnable', 'ports.bases',
115
+ 'environment.install_policies', 'environment.test_data', 'environment.compose_files', 'environment.machine', 'environment.posture',
116
+ 'testing', 'guards']);
117
+ // A declared route's name or its path; the command checks it against the declared routes.
118
+ const ROUTE_SELECTOR = /^(?:[a-z0-9][a-z0-9_-]{0,63}|\/(?!\/)[^\s#\x00-\x1f\x7f]{0,511})$/;
100
119
  const EXACT_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
101
120
  const flag = Object.freeze({ kind: 'flag', role: 'boolean', repetition: 'singleton' });
102
121
  function value(role, required = false) {
@@ -108,6 +127,8 @@ const configFlag = str(value('resolved-config', true));
108
127
  const jsonFlag = bool(flag);
109
128
  const configured = { config: configFlag, json: jsonFlag };
110
129
  const workOrderFlag = str(value('identifier', true));
130
+ const profileFlag = { type: 'string', multiple: true, agent: { kind: 'value', role: 'compose-profile', repetition: 'repeated' } };
131
+ const serviceFlag = str(value('declared-service', true));
111
132
  // The flags and their roles are filled in from `flags` when the table is built.
112
133
  function ordinary(command, extra = {}) {
113
134
  return { availability: 'available', executableRole: 'installed-cockpit', command,
@@ -150,6 +171,28 @@ const sources = [
150
171
  ordinary: ordinary('setup', { forbidden: Object.freeze({ presence: Object.freeze(['write-instruction-pointers', 'write-instruction-files']) }) }),
151
172
  flags: { config: configFlag, 'write-instruction-pointers': bool(), 'write-instruction-files': bool(), json: jsonFlag },
152
173
  },
174
+ {
175
+ name: 'use',
176
+ stage: 'set-up',
177
+ summary: 'Serve a sibling from another of its checkouts, such as a worktree',
178
+ usage: '<slug> <checkout> | <slug> --clear [options]',
179
+ handler: 'use',
180
+ visibility: 'public',
181
+ // The checkout must already lie inside a directory the guard lets the agent write, so an agent
182
+ // can point the stack at its own worktree but never widen its reach to a new directory.
183
+ ordinary: ordinary('use', { positional: Object.freeze({ min: 1, max: 2, roles: Object.freeze(['slug', 'confined-root']) }) }),
184
+ flags: { config: configFlag, clear: bool(flag), json: jsonFlag },
185
+ },
186
+ {
187
+ name: 'pull',
188
+ stage: 'set-up',
189
+ summary: 'Fast-forward the cockpit and its siblings to their upstreams',
190
+ handler: 'pull',
191
+ visibility: 'public',
192
+ // Fetch and a fast-forward only: it never merges, rebases, stashes or touches a modified checkout.
193
+ ordinary: ordinary('pull'),
194
+ flags: configured,
195
+ },
153
196
  {
154
197
  name: 'sibling',
155
198
  stage: 'set-up',
@@ -168,9 +211,25 @@ const sources = [
168
211
  usage: `add|remove <${SURFACE_TARGETS.join('|')}> [options]`,
169
212
  handler: 'tool',
170
213
  visibility: 'public',
171
- // Classified with sibling add and remove: both rewrite cockpit.toml.
172
- ordinary: unavailable('tool add and remove rewrite cockpit.toml'),
173
- flags: { config: str(), 'dry-run': bool(), json: bool() },
214
+ // tool add edits only [surfaces] tools, and an added tool never widens what agents may do, so the
215
+ // agent syncs it as [autonomy] sync allows. tool remove deletes that tool's guard, so it is a person's.
216
+ ordinary: ordinary('tool', { positional: Object.freeze({ min: 2, max: 2, roles: Object.freeze(['operation', 'surface-target']),
217
+ operations: Object.freeze(['add']) }) }),
218
+ flags: { config: configFlag, 'dry-run': bool(flag), json: jsonFlag },
219
+ },
220
+ {
221
+ name: 'confirm',
222
+ stage: 'set-up',
223
+ summary: 'Confirm an init answer no person stated by running the observation that verifies it',
224
+ usage: '<answer> [options]',
225
+ handler: 'confirm',
226
+ visibility: 'public',
227
+ // It removes an entry from unconfirmed only after an observation of this machine and the running
228
+ // stack passes, and records that observation where only cockpit writes; the guard then treats the
229
+ // removal as non-widening. It refuses the answers a person must confirm.
230
+ ordinary: ordinary('confirm', { positional: Object.freeze({ min: 1, max: 1, roles: Object.freeze(['operation']),
231
+ operations: UNCONFIRMED_ANSWER_WORDS }) }),
232
+ flags: configured,
174
233
  },
175
234
  {
176
235
  name: 'registry-resolve',
@@ -204,7 +263,8 @@ const sources = [
204
263
  // Adoption rewrites cockpit.toml and moves a team's harness settings, so it stays a person's. A
205
264
  // real sync rewrites the guard's own files from a configuration the guard already pins and
206
265
  // holds every call after an edit to, so it is an agent's only where [autonomy] sync = "agent".
207
- ordinary: ordinary('sync', { forbidden: Object.freeze({ presence: Object.freeze(['interactive', 'adopt']) }), requires: 'autonomy-sync' }),
266
+ ordinary: ordinary('sync', { forbidden: Object.freeze({ presence: Object.freeze(['interactive', 'adopt']) }), requires: 'autonomy-sync',
267
+ admittedWhileUnsynced: true }),
208
268
  flags: { config: configFlag, 'dry-run': bool(flag), adopt: { type: 'string', multiple: true }, interactive: bool(), json: jsonFlag },
209
269
  },
210
270
  {
@@ -213,7 +273,7 @@ const sources = [
213
273
  summary: 'Report generated files that no longer match cockpit.toml',
214
274
  handler: 'check',
215
275
  visibility: 'public',
216
- ordinary: ordinary('check'),
276
+ ordinary: ordinary('check', { admittedWhileUnsynced: true }),
217
277
  flags: configured,
218
278
  },
219
279
  {
@@ -247,7 +307,7 @@ const sources = [
247
307
  handler: 'up',
248
308
  visibility: 'public',
249
309
  ordinary: ordinary('up'),
250
- flags: configured,
310
+ flags: { ...configured, profile: profileFlag },
251
311
  },
252
312
  {
253
313
  name: 'preflight',
@@ -265,9 +325,10 @@ const sources = [
265
325
  usage: '[options] -- <command> [args...]',
266
326
  handler: 'session',
267
327
  visibility: 'public',
268
- // The guard admits `session --config <file> -- <command>` only when it admits the inner command.
328
+ // The guard admits `session --config <file> [--profile <name>]... -- <command>` only when it
329
+ // admits the inner command.
269
330
  ordinary: unavailable('session wraps an arbitrary foreground command'),
270
- flags: { config: str() },
331
+ flags: { config: str(), profile: { type: 'string', multiple: true } },
271
332
  },
272
333
  {
273
334
  name: 'logs',
@@ -276,9 +337,49 @@ const sources = [
276
337
  handler: 'logs',
277
338
  visibility: 'public',
278
339
  ordinary: ordinary('logs'),
279
- flags: { config: configFlag, service: str(value('declared-service', true)), tail: str(value('tail'), '100'), since: str(value('since'), '15m'),
340
+ flags: { config: configFlag, service: serviceFlag, tail: str(value('tail'), '100'), since: str(value('since'), '15m'),
280
341
  follow: bool(flag), json: jsonFlag },
281
342
  },
343
+ {
344
+ name: 'ps',
345
+ stage: 'run-the-stack',
346
+ summary: 'Show the state and health of each service in this checkout\'s stack',
347
+ handler: 'ps',
348
+ visibility: 'public',
349
+ ordinary: ordinary('ps'),
350
+ flags: configured,
351
+ },
352
+ // The service verbs act only on this checkout's own leased stack, as up does, so an agent runs
353
+ // them as it runs up; reset, which deletes data, stays gated.
354
+ {
355
+ name: 'restart',
356
+ stage: 'run-the-stack',
357
+ summary: 'Restart one service of this checkout\'s stack and wait until it is healthy',
358
+ handler: 'restart',
359
+ visibility: 'public',
360
+ ordinary: ordinary('restart'),
361
+ flags: { ...configured, service: serviceFlag },
362
+ },
363
+ {
364
+ name: 'rebuild',
365
+ stage: 'run-the-stack',
366
+ summary: 'Rebuild one service\'s image, replace its container and wait until it is healthy',
367
+ handler: 'rebuild',
368
+ visibility: 'public',
369
+ ordinary: ordinary('rebuild'),
370
+ flags: { ...configured, service: serviceFlag },
371
+ },
372
+ {
373
+ name: 'exec',
374
+ stage: 'run-the-stack',
375
+ summary: 'Run a command inside one service\'s container of this checkout\'s stack',
376
+ usage: '--service <name> [options] -- <command> [args...]',
377
+ handler: 'exec',
378
+ visibility: 'public',
379
+ // The grammar admits the spelling; the evaluator judges the container command.
380
+ ordinary: ordinary('exec'),
381
+ flags: { config: configFlag, service: serviceFlag },
382
+ },
282
383
  {
283
384
  name: 'sleep',
284
385
  stage: 'run-the-stack',
@@ -308,6 +409,21 @@ const sources = [
308
409
  ordinary: ordinary('reset', { requires: 'autonomy-reset' }),
309
410
  flags: { config: configFlag, interactive: bool(), apply: bool(Object.freeze({ ...flag, required: true })), json: jsonFlag },
310
411
  },
412
+ {
413
+ name: 'stacks',
414
+ stage: 'run-the-stack',
415
+ summary: 'List every stack on this machine, or preview and remove those whose checkout is gone (stacks prune)',
416
+ usage: '[prune] [options]',
417
+ handler: 'stacks',
418
+ visibility: 'public',
419
+ // Listing and the prune preview change nothing. --apply removes only stacks whose checkout is
420
+ // gone; the command itself refuses one that deletes volumes unless [autonomy] reset allows it.
421
+ ordinary: ordinary('stacks', {
422
+ positional: Object.freeze({ min: 0, max: 1, roles: Object.freeze(['operation']), operations: Object.freeze(['prune']) }),
423
+ ...personFlag,
424
+ }),
425
+ flags: { config: configFlag, apply: bool(flag), interactive: bool(), json: jsonFlag },
426
+ },
311
427
  {
312
428
  name: 'doctor',
313
429
  stage: 'diagnose',
@@ -333,12 +449,20 @@ const sources = [
333
449
  { name: 'work check', handler: 'work-check', stage: 'test-and-grade', summary: "Run every criterion's check and the order's gates", visibility: 'public',
334
450
  ordinary: ordinary('work-check'),
335
451
  flags: { ...configured, 'work-order': workOrderFlag } },
336
- { name: 'work note', handler: 'work-note', stage: 'test-and-grade', summary: 'Record a note on an order, or with --ask an open question that blocks it', visibility: 'public',
452
+ { name: 'work note', handler: 'work-note', stage: 'test-and-grade', summary: 'Record a note on an order, an open question with --ask, or a decision with --decision', visibility: 'public',
337
453
  ordinary: ordinary('work-note'),
338
- flags: { ...configured, 'work-order': workOrderFlag, text: str(value('label', true)), ask: bool(flag) } },
454
+ flags: { ...configured, 'work-order': workOrderFlag, text: str(value('label', true)), ask: bool(flag), decision: bool(flag) } },
455
+ // Resolving a decision passes no gate of its own: a --none resolution is shown in the signoff prompt, which the signoff gate covers.
456
+ { name: 'work decide', handler: 'work-decide', stage: 'test-and-grade', summary: 'Resolve a recorded decision: write or cite its decision record, or say it is not lasting', visibility: 'public',
457
+ ordinary: ordinary('work-decide'),
458
+ flags: { ...configured, 'work-order': workOrderFlag, decision: str(value('tail', true)), record: bool(flag), 'record-file': str(value('label')),
459
+ supersedes: str(value('label')), none: bool(flag), reason: str(value('label')) } },
339
460
  { name: 'work answer', handler: 'work-answer', stage: 'test-and-grade', summary: 'Answer an open question on an order, as [work.gates] allows', visibility: 'public',
340
461
  ordinary: ordinary('work-answer', personFlag),
341
462
  flags: { ...configured, 'work-order': workOrderFlag, question: str(value('tail', true)), text: str(value('label', true)), interactive: bool() } },
463
+ { name: 'work acknowledge', handler: 'work-acknowledge', stage: 'test-and-grade', summary: 'Acknowledge that an order weakened tests, as [work.gates] allows', visibility: 'public',
464
+ ordinary: ordinary('work-acknowledge', personFlag),
465
+ flags: { ...configured, 'work-order': workOrderFlag, note: str(value('label', true)), interactive: bool() } },
342
466
  // Grading hands the order to an evaluator running apart from the agent that did the work, unless
343
467
  // [work] grading names checks or self.
344
468
  { name: 'work grade', handler: 'work-grade', stage: 'test-and-grade', summary: 'Grade an accepted order the way [work] grading says', visibility: 'public',
@@ -347,12 +471,11 @@ const sources = [
347
471
  { name: 'work signoff', handler: 'work-signoff', stage: 'test-and-grade', summary: 'Close a graded order as done, or any open one as partial or abandoned', visibility: 'public',
348
472
  ordinary: ordinary('work-signoff', personFlag),
349
473
  flags: { ...configured, 'work-order': workOrderFlag, partial: bool(flag), abandon: bool(flag), note: str(value('label')), interactive: bool() } },
350
- // Inspection only reports the holder. --apply deletes the lock once a pid probe calls its holder
351
- // dead, and that probe sees only this machine's process table: a holder running in a container or
352
- // on another host that shares the checkout looks dead to it, so a person confirms it is gone.
474
+ // Inspection only reports the holder. --apply removes the lock only once the command has shown its
475
+ // holder gone where a pid probe cannot see: the lock's host and the containers cockpit labels.
353
476
  { name: 'work recover', handler: 'work-recover', stage: 'test-and-grade', summary: "Inspect or remove a crashed command's work-order lock", visibility: 'public',
354
- ordinary: ordinary('work-recover', { forbidden: Object.freeze({ presence: Object.freeze(['apply']) }) }),
355
- flags: { ...configured, 'work-order': workOrderFlag, apply: bool() } },
477
+ ordinary: ordinary('work-recover'),
478
+ flags: { ...configured, 'work-order': workOrderFlag, apply: bool(flag) } },
356
479
  { name: 'work verify', handler: 'work-verify', stage: 'test-and-grade', summary: 'Check every record against its order; in CI, pass the steps gated on ci', visibility: 'public',
357
480
  ordinary: ordinary('work-verify'),
358
481
  flags: configured },
@@ -363,10 +486,15 @@ const sources = [
363
486
  // person wrote down as safe for that.
364
487
  { name: 'browser drive', handler: 'browser-drive', stage: 'test-and-grade', summary: 'Drive the declared application browser surfaces', visibility: 'public',
365
488
  ordinary: ordinary('browser-drive', browserAdmission),
366
- flags: { ...configured, role: str(value('identifier')) } },
489
+ // Findings are evidence by default; this makes them fail the command, for a criterion that wants a clean route.
490
+ flags: { ...configured, role: str(value('identifier')), route: str(value('route')), 'fail-on-findings': bool(flag) } },
367
491
  { name: 'browser screenshot', handler: 'browser-screenshot', stage: 'test-and-grade', summary: 'Capture the declared application surfaces', visibility: 'public',
368
492
  ordinary: ordinary('browser-screenshot', browserAdmission),
369
- flags: { ...configured, role: str(value('identifier')) } },
493
+ flags: { ...configured, role: str(value('identifier')), route: str(value('route')) } },
494
+ // Reads documents and runs read-only Git, like check; it writes nothing, so an agent runs it freely.
495
+ { name: 'docs check', handler: 'docs-check', stage: 'test-and-grade', summary: 'Report removed paths and make targets living docs still cite, and broken decision records', visibility: 'public',
496
+ ordinary: ordinary('docs-check'),
497
+ flags: configured },
370
498
  ];
371
499
  export const commands = sources.map(({ flags, ...command }) => {
372
500
  const entries = Object.entries(flags);
@@ -384,6 +512,8 @@ export const internalCommands = Object.freeze([Object.freeze({
384
512
  const ORDINARY_GRANTS = ['browser-preapproval', 'autonomy-sync', 'autonomy-reset'];
385
513
  // An image reference or ID as Docker spells it; the command itself refuses one that is not local.
386
514
  const LOCAL_IMAGE = /^[A-Za-z0-9][A-Za-z0-9._/:@-]{0,255}$/;
515
+ // The spelling environment.profiles accepts.
516
+ const COMPOSE_PROFILE = /^[a-z0-9][a-z0-9_.-]{0,63}$/;
387
517
  // A selected value that starts with `-` would reach the product's tool as an option of its own.
388
518
  const GATE_SELECTOR = /^(?:spec|file|test|route):[^-]/;
389
519
  function canonicalPath(path) {
@@ -394,19 +524,31 @@ function canonicalPath(path) {
394
524
  function record(value) {
395
525
  return value !== null && typeof value === 'object' && !Array.isArray(value);
396
526
  }
527
+ /** The commands the guard admits while a non-widening edit of cockpit.toml waits for sync. */
528
+ export const UNSYNCED_ADMITTED_COMMANDS = Object.freeze(commands.flatMap(row => row.ordinary.availability === 'available' && row.ordinary.admittedWhileUnsynced ? [row.name] : []));
529
+ /** The same commands as prose, for the hold that names them: "cockpit sync and check". */
530
+ export const UNSYNCED_ADMITTED_PROSE = `cockpit ${new Intl.ListFormat('en', { type: 'conjunction' }).format(UNSYNCED_ADMITTED_COMMANDS)}`;
397
531
  /**
398
532
  * The inner command of an exact `session --config <configured file> -- <command…>` run from the
399
533
  * cockpit root, or undefined. The guard admits the session only when it admits the inner command,
400
534
  * so a session never runs what the agent could not run directly.
401
535
  */
402
536
  export function ordinarySessionCommand(argv, cwd, authority) {
403
- const separator = argv.indexOf('--');
404
- if (argv[0] !== 'session' || separator < 0 || separator === argv.length - 1 || cwd !== authority.canonicalRoot)
537
+ const inner = innerCommand(argv);
538
+ if (argv[0] !== 'session' || !inner || cwd !== authority.canonicalRoot)
405
539
  return undefined;
406
- const options = argv.slice(1, separator);
407
- const config = options.length === 2 && options[0] === '--config' ? options[1]
408
- : options.length === 1 && options[0].startsWith('--config=') ? options[0].slice('--config='.length) : undefined;
409
- return config && resolve(cwd, config) === authority.configPath ? argv.slice(separator + 1) : undefined;
540
+ const options = argv.slice(1, argv.length - inner.words.length - 1);
541
+ let config;
542
+ for (let index = 0; index < options.length; index++) {
543
+ const [name, inline] = options[index].startsWith('--') && options[index].includes('=')
544
+ ? [options[index].slice(0, options[index].indexOf('=')), options[index].slice(options[index].indexOf('=') + 1)]
545
+ : [options[index], options[++index]];
546
+ if (name === '--config' && config === undefined && inline !== undefined)
547
+ config = inline;
548
+ else if (name !== '--profile' || inline === undefined || !authority.profiles.includes(inline))
549
+ return undefined;
550
+ }
551
+ return config && resolve(cwd, config) === authority.configPath ? inner.words : undefined;
410
552
  }
411
553
  /** Classify one exact candidate; the caller authenticates filesystem and executable authority. */
412
554
  export function resolveOrdinaryInvocation(input, authority, inspectPath) {
@@ -415,7 +557,7 @@ export function resolveOrdinaryInvocation(input, authority, inspectPath) {
415
557
  if (!record(candidate)
416
558
  || Object.keys(candidate).sort().join(',') !== 'argv,cwd,executable'
417
559
  || !record(captured)
418
- || Object.keys(captured).sort().join(',') !== 'allowedRoots,canonicalRoot,configPath,executable,gates,grants,services')
560
+ || Object.keys(captured).sort().join(',') !== 'allowedRoots,canonicalRoot,configPath,executable,gates,grants,profiles,services')
419
561
  return undefined;
420
562
  const source = captured;
421
563
  if (!canonicalPath(source.executable) || !canonicalPath(source.canonicalRoot) || !canonicalPath(source.configPath)
@@ -424,6 +566,7 @@ export function resolveOrdinaryInvocation(input, authority, inspectPath) {
424
566
  || !Array.isArray(source.services) || !source.services.every(service => typeof service === 'string' && /^[a-z0-9][a-z0-9_.-]{0,127}$/.test(service))
425
567
  || !Array.isArray(source.gates) || !source.gates.every(gate => typeof gate === 'string' && /^[a-z][a-z0-9-]{0,63}$/.test(gate))
426
568
  || !Array.isArray(source.grants) || !source.grants.every(grant => ORDINARY_GRANTS.includes(grant))
569
+ || !Array.isArray(source.profiles) || !source.profiles.every(profile => typeof profile === 'string' && COMPOSE_PROFILE.test(profile))
427
570
  || candidate.executable !== source.executable || candidate.cwd !== source.canonicalRoot
428
571
  || !Array.isArray(candidate.argv) || candidate.argv.length === 0 || candidate.argv.length > GUARD_INPUT_LIMITS.words
429
572
  || !candidate.argv.every(arg => typeof arg === 'string' && arg.length > 0
@@ -452,7 +595,12 @@ export function resolveOrdinaryInvocation(input, authority, inspectPath) {
452
595
  const reads = [];
453
596
  const positionals = [];
454
597
  const options = new Map();
455
- const words = spelling.rest;
598
+ const wraps = WRAPPING_COMMANDS.has(definition.name);
599
+ const separator = wraps ? spelling.rest.indexOf('--') : -1;
600
+ // The wrapped command is required and is the only thing after `--`.
601
+ if (wraps && (separator < 0 || separator === spelling.rest.length - 1))
602
+ return undefined;
603
+ const words = separator < 0 ? spelling.rest : spelling.rest.slice(0, separator);
456
604
  for (let index = 0; index < words.length; index++) {
457
605
  const arg = words[index];
458
606
  if (!arg.startsWith('-')) {
@@ -464,7 +612,7 @@ export function resolveOrdinaryInvocation(input, authority, inspectPath) {
464
612
  const equal = arg.indexOf('=');
465
613
  const name = arg.slice(2, equal < 0 ? undefined : equal);
466
614
  const option = grammar.options[name];
467
- if (!option || options.has(name) || grammar.forbidden.presence.includes(name))
615
+ if (!option || options.has(name) && option.repetition !== 'repeated' || grammar.forbidden.presence.includes(name))
468
616
  return undefined;
469
617
  if (option.kind === 'flag') {
470
618
  if (equal >= 0)
@@ -484,15 +632,17 @@ export function resolveOrdinaryInvocation(input, authority, inspectPath) {
484
632
  : option.role === 'gate-mode' ? ['canonical', 'full', 'staged'].includes(text)
485
633
  : option.role === 'gate-selector' ? GATE_SELECTOR.test(text)
486
634
  : option.role === 'identifier' ? WORK_ORDER_ID.test(text)
487
- : option.role === 'label' ? true
488
- : option.role === 'field' ? isRecordedAnswerField(text)
489
- : option.role === 'tail' ? /^[1-9]\d{0,4}$/.test(text) && Number(text) <= 10_000
490
- : option.role === 'since' ? text.length <= 64 && (/^\d+(?:\.\d+)?[smhd]$/.test(text)
491
- || /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)$/.test(text) && Number.isFinite(Date.parse(text)))
492
- : option.role === 'surface-target' ? text.split(',').every(tool => SURFACE_TARGETS.some(target => target === tool))
493
- : option.role === 'evaluator-tool' ? EVALUATOR_TOOLS.includes(text)
494
- : option.role === 'local-image' ? LOCAL_IMAGE.test(text)
495
- : option.role === 'sha256' && /^[a-f0-9]{64}$/.test(text);
635
+ : option.role === 'route' ? ROUTE_SELECTOR.test(text)
636
+ : option.role === 'label' ? true
637
+ : option.role === 'field' ? isRecordedAnswerField(text)
638
+ : option.role === 'tail' ? /^[1-9]\d{0,4}$/.test(text) && Number(text) <= 10_000
639
+ : option.role === 'since' ? text.length <= 64 && (/^\d+(?:\.\d+)?[smhd]$/.test(text)
640
+ || /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,9})?(?:Z|[+-]\d\d:\d\d)$/.test(text) && Number.isFinite(Date.parse(text)))
641
+ : option.role === 'surface-target' ? text.split(',').every(tool => SURFACE_TARGETS.some(target => target === tool))
642
+ : option.role === 'evaluator-tool' ? EVALUATOR_TOOLS.includes(text)
643
+ : option.role === 'local-image' ? LOCAL_IMAGE.test(text)
644
+ : option.role === 'compose-profile' ? source.profiles.includes(text)
645
+ : option.role === 'sha256' && /^[a-f0-9]{64}$/.test(text);
496
646
  if (!valid)
497
647
  return undefined;
498
648
  options.set(name, text);
@@ -501,7 +651,9 @@ export function resolveOrdinaryInvocation(input, authority, inspectPath) {
501
651
  || positionals.some((word, index) => grammar.positional.roles[index] === 'operation'
502
652
  ? !grammar.positional.operations?.includes(word)
503
653
  : grammar.positional.roles[index] === 'version' ? !EXACT_VERSION.test(word)
504
- : grammar.positional.roles[index] !== 'confined-root' || !confined(word))
654
+ : grammar.positional.roles[index] === 'slug' ? !/^[a-z0-9][a-z0-9-]{0,62}$/.test(word)
655
+ : grammar.positional.roles[index] === 'surface-target' ? !SURFACE_TARGETS.some(target => target === word)
656
+ : grammar.positional.roles[index] !== 'confined-root' || !confined(word))
505
657
  || Object.entries(grammar.options).some(([name, option]) => option.required && !options.has(name))
506
658
  // A dry run writes nothing, so no grant gates it.
507
659
  || grammar.requires !== undefined && !options.has('dry-run') && !source.grants.includes(grammar.requires)
@@ -1,9 +1,14 @@
1
1
  import { type ConfigSourceSnapshot, type EnvironmentCommandOutcome, type EnvironmentDiagnostic, type SourceProvenance } from '../environment/contract.js';
2
2
  import { type EffectivePolicy } from '../autonomy.js';
3
- import { CockpitConfig, type EvaluatorTool } from '../schema.js';
3
+ import { browserDataQualifies, CockpitConfig, type EvaluatorTool } from '../schema.js';
4
+ /** An explicit request_preapproval wins; otherwise preapproval follows the declared data posture. */
5
+ export declare const browserPreapprovalRequested: (browser: Readonly<{
6
+ request_preapproval?: boolean | undefined;
7
+ data: Parameters<typeof browserDataQualifies>[0];
8
+ }>) => boolean;
4
9
  import { type BoundedRead } from '../bounded-read.js';
5
10
  export declare const STRUCTURED_INPUT_BYTE_LIMIT = 1048576;
6
- export type BoundedInputRead = BoundedRead;
11
+ type BoundedInputRead = BoundedRead;
7
12
  export declare function readBoundedInput(path: string): Promise<BoundedInputRead>;
8
13
  declare const loadedConfigProjectionBrand: unique symbol;
9
14
  export type LoadedConfigProjection = LoadedCockpitConfig;
@@ -29,7 +34,7 @@ export interface LoadedCockpitConfig {
29
34
  export interface MachineFloorConfigProjection {
30
35
  readonly kind: 'machine-floor-config-projection';
31
36
  }
32
- export interface ConsumedMachineFloorConfig {
37
+ interface ConsumedMachineFloorConfig {
33
38
  readonly cockpitName: string;
34
39
  readonly source: SourceProvenance;
35
40
  readonly runtimeSha256: string;
@@ -39,7 +44,7 @@ export interface ConsumedMachineFloorConfig {
39
44
  freeDiskMb: number;
40
45
  }>;
41
46
  }
42
- export interface WorkEnvironmentConfigProjection {
47
+ interface WorkEnvironmentConfigProjection {
43
48
  readonly kind: 'work-environment-config-projection';
44
49
  }
45
50
  export interface ConsumedWorkEnvironmentConfig {
@@ -66,6 +71,10 @@ export interface ConsumedWorkEnvironmentConfig {
66
71
  readonly generatedHostPaths: readonly string[];
67
72
  readonly browser?: ReturnType<typeof projectBrowserConfig>;
68
73
  readonly testing?: ReturnType<typeof projectTestingConfig>;
74
+ readonly docs?: Readonly<{
75
+ decisions?: string;
76
+ decisionsIndex?: string;
77
+ }>;
69
78
  }
70
79
  declare function projectBrowserConfig(declaredBrowser: NonNullable<CockpitConfig['browser']>): {
71
80
  engine: "chromium" | "firefox" | "webkit";
@@ -139,6 +148,7 @@ declare function projectTestingConfig(declaredTesting: NonNullable<CockpitConfig
139
148
  }[];
140
149
  };
141
150
  nativeHookInstall: "explicit-native" | "inspect";
151
+ testFiles?: string[] | undefined;
142
152
  nativeHookPolicy?: {
143
153
  path: string;
144
154
  sha256: string;
@@ -162,6 +172,8 @@ export declare function loadAutonomyPolicy(configPath: string): Promise<Readonly
162
172
  }> | Readonly<{
163
173
  refusal: EnvironmentDiagnostic;
164
174
  }>>;
175
+ /** The configuration `bytes` declare, or undefined when they are not UTF-8 TOML the schema accepts. */
176
+ export declare function parseCockpitConfigBytes(bytes: Uint8Array): CockpitConfig | undefined;
165
177
  export declare function loadCockpitConfig(inputPath: string): Promise<EnvironmentCommandOutcome<LoadedCockpitConfig>>;
166
178
  /**
167
179
  * The evaluators work grade and cockpit grade try, in order. Left unset, they are the listed agent