@microck/canonfig 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +263 -0
  3. package/dist/agent/agent-resolution.errors.js +42 -0
  4. package/dist/agent/agent-resolution.layer.js +204 -0
  5. package/dist/agent/agent-resolution.service.js +2259 -0
  6. package/dist/agent/agent-resolution.types.js +1 -0
  7. package/dist/agent/controlled-executor.js +704 -0
  8. package/dist/agent/harness-adapters.js +85 -0
  9. package/dist/cli/cli.js +618 -0
  10. package/dist/cli/exit-codes.js +28 -0
  11. package/dist/cli/follower-commands.js +3 -0
  12. package/dist/cli/render.js +56 -0
  13. package/dist/cli/source-commands.js +5 -0
  14. package/dist/domain/brand.js +29 -0
  15. package/dist/domain/identity.js +31 -0
  16. package/dist/domain/npm-package-spec.js +186 -0
  17. package/dist/domain/profile.js +950 -0
  18. package/dist/domain/recipe-versions.js +297 -0
  19. package/dist/domain/resource.js +259 -0
  20. package/dist/domain/synchronization.js +346 -0
  21. package/dist/enrollment/enrollment.errors.js +43 -0
  22. package/dist/enrollment/enrollment.layer.js +724 -0
  23. package/dist/enrollment/enrollment.service.js +3 -0
  24. package/dist/enrollment/enrollment.types.js +59 -0
  25. package/dist/enrollment/follower-client.js +585 -0
  26. package/dist/enrollment/source-server.js +313 -0
  27. package/dist/machine/linux.layer.js +1183 -0
  28. package/dist/machine/machine-state.errors.js +52 -0
  29. package/dist/machine/machine-state.service.js +3 -0
  30. package/dist/machine/machine-state.types.js +1 -0
  31. package/dist/machine/macos.layer.js +470 -0
  32. package/dist/machine/windows.layer.js +879 -0
  33. package/dist/profile/discovery.js +740 -0
  34. package/dist/profile/profile-catalog.errors.js +50 -0
  35. package/dist/profile/profile-catalog.layer.js +20 -0
  36. package/dist/profile/profile-catalog.service.js +7 -0
  37. package/dist/profile/profile-codec.js +153 -0
  38. package/dist/profile/publication.js +298 -0
  39. package/dist/profile/tool-catalog.js +384 -0
  40. package/dist/runtime/doctor.js +306 -0
  41. package/dist/runtime/layers.js +706 -0
  42. package/dist/runtime/main.js +38 -0
  43. package/dist/schedule/linux-schedule.js +24 -0
  44. package/dist/schedule/macos-schedule.js +25 -0
  45. package/dist/schedule/schedule-manager.errors.js +17 -0
  46. package/dist/schedule/schedule-manager.layer.js +205 -0
  47. package/dist/schedule/schedule-manager.service.js +3 -0
  48. package/dist/schedule/schedule-manager.types.js +114 -0
  49. package/dist/schedule/windows-schedule.js +25 -0
  50. package/dist/state/state-repository.errors.js +55 -0
  51. package/dist/state/state-repository.layer.js +1507 -0
  52. package/dist/state/state-repository.service.js +3 -0
  53. package/dist/state/state-repository.types.js +1 -0
  54. package/dist/state/state-schema.js +298 -0
  55. package/dist/synchronization/config-codec.js +97 -0
  56. package/dist/synchronization/executor.js +700 -0
  57. package/dist/synchronization/follower-orchestration.js +939 -0
  58. package/dist/synchronization/follower-sync-config.js +81 -0
  59. package/dist/synchronization/npm-artifact.js +670 -0
  60. package/dist/synchronization/planner.js +378 -0
  61. package/dist/synchronization/recovery.js +397 -0
  62. package/dist/synchronization/resource-executors.js +1198 -0
  63. package/dist/synchronization/resource-plans.js +645 -0
  64. package/dist/synchronization/synchronization.errors.js +102 -0
  65. package/dist/synchronization/synchronization.layer.js +97 -0
  66. package/dist/synchronization/synchronization.service.js +11 -0
  67. package/dist/synchronization/synchronization.types.js +1 -0
  68. package/package.json +66 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Microck
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,263 @@
1
+ # Canonfig
2
+
3
+ Canonfig 2 is a deterministic, one-way configuration synchronizer for AI agent
4
+ setups. One **Source Machine** discovers and explicitly publishes immutable,
5
+ authenticated **Profile Revisions**. Linux, macOS, and Windows **Follower
6
+ Machines** fetch only revisions allowed for their identities and groups, then
7
+ plan, apply, and independently verify the selected profile.
8
+
9
+ Configuration Agents are optional and bounded. Canonfig always runs declared
10
+ deterministic actions first, and an agent statement never counts as proof of
11
+ convergence.
12
+
13
+ ## What Canonfig manages
14
+
15
+ A Machine Profile can declare these resource kinds and apply policies:
16
+
17
+ | Resource | Default policy | Outcome |
18
+ | --- | --- | --- |
19
+ | File | `replace` | Exact owned file or symlink content |
20
+ | Directory | `mirror-owned` | Source-owned tree; only unchanged owned files are removed |
21
+ | Config | `merge` | Declared TOML, JSON, or YAML keys with local keys preserved |
22
+ | Skill | `replace-if-unmodified` | Canonical skill tree without overwriting follower edits |
23
+ | Tool | `ensure` | Platform-specific installation and verification |
24
+ | Credential | `require-local` | Usable local credential reference, never a copied secret |
25
+ | Schedule | `replace` | Native user-level synchronization schedule |
26
+
27
+ Transfers are content-addressed and incremental. Transfer and apply are separate:
28
+ a downloaded blob is not proof that a resource converged.
29
+
30
+ ## Supported platforms
31
+
32
+ Canonfig requires Node.js 24 or newer and npm.
33
+
34
+ | Platform | Secure credential provider | Native user scheduler | Common recipes |
35
+ | --- | --- | --- | --- |
36
+ | Linux | Secret Service | systemd user timer | apt, npm, uv, cargo, source |
37
+ | macOS | Keychain | launchd user agent | Homebrew, npm, uv, cargo, source |
38
+ | Windows 10/11 | Credential Manager | per-user Task Scheduler | winget, npm, uv, cargo, source |
39
+
40
+ Paths, package identities, recipes, credential providers, and scheduler
41
+ artifacts remain platform-specific even when the desired capability is shared.
42
+
43
+ Package recipes default to fail-closed builds: npm uses `--ignore-scripts` and
44
+ uv uses `--only-binary=:all:`. Recipes that require lifecycle hooks or source
45
+ distributions must include a reviewed build policy with bounded toolchain
46
+ executables, paths, HTTPS origins, capabilities, and steps. The current
47
+ executor escalates those recipes to Human Action Required because it cannot
48
+ confine lifecycle descendants. Git and other source dependency specifications
49
+ are not accepted without a separately bounded execution plan. Reviewed source
50
+ recipes are preserved as immutable references but always require Human Action
51
+ Required; the current executor never fetches or builds source code.
52
+
53
+ ## Install
54
+
55
+ Install the exact public package version on Linux, macOS, or Windows:
56
+
57
+ ```bash
58
+ npm install --global @microck/canonfig@2.0.0
59
+ canonfig --version
60
+ ```
61
+
62
+ The npm package is scoped as `@microck/canonfig`; the installed executable
63
+ remains `canonfig`.
64
+
65
+ ## Quickstart
66
+
67
+ ### 1. Initialize the Source Machine
68
+
69
+ ```bash
70
+ canonfig source init
71
+ canonfig doctor --no-input --timeout-ms 5000
72
+ ```
73
+
74
+ Discover explicit source evidence, review the resulting proposal, and publish it
75
+ intentionally:
76
+
77
+ ```bash
78
+ canonfig source scan --file AGENTS.md --file package.json
79
+ canonfig source publish --proposal package.json --profile workstation --name Workstation --reviewer operator
80
+ canonfig source publish --profile-file ~/.canonfig/source/profile.jsonc --proposal package.json --reviewer operator
81
+ canonfig profile list
82
+ ```
83
+
84
+ The JSONC `--profile-file` form is authoritative for the profile id, name,
85
+ groups, resources, policies, dependencies, and schedule default. Accepted
86
+ discovery proposals are merged for resource ids not authored in that file;
87
+ unreviewed discovery remains blocked, and duplicate or conflicting resource
88
+ targets fail closed. Credentials are references only and are never copied from
89
+ the Source Machine.
90
+
91
+ ### 2. Serve and invite
92
+
93
+ The currently shipped source server accepts loopback hosts only:
94
+
95
+ ```bash
96
+ canonfig source serve --host 127.0.0.1 --port 17342
97
+ ```
98
+
99
+ While it is running, create a short-lived, single-use invitation:
100
+
101
+ ```bash
102
+ canonfig source invite --endpoint https://127.0.0.1:17342 --expires 15m --group developers
103
+ ```
104
+
105
+ Treat the returned invitation as temporary sensitive material. The enrolled
106
+ endpoint must be reachable as that exact HTTPS origin.
107
+
108
+ ### 3. Enroll a follower
109
+
110
+ Keep the complete invitation in an ephemeral local variable:
111
+
112
+ ```bash
113
+ canonfig follower enroll "$INVITE" --name laptop --profile workstation
114
+ canonfig profile select workstation
115
+ canonfig sync --plan
116
+ ```
117
+
118
+ Enrollment pins the source TLS and signing fingerprints and issues an
119
+ independently revocable follower credential. Apply only after reviewing the
120
+ plan:
121
+
122
+ ```bash
123
+ canonfig sync --apply
124
+ canonfig status
125
+ ```
126
+
127
+ ### 4. Configure synchronization
128
+
129
+ The default follower schedule is daily at 00:00 in its configured local
130
+ timezone:
131
+
132
+ ```bash
133
+ canonfig schedule set daily@00:00
134
+ canonfig schedule status
135
+ ```
136
+
137
+ Native jobs invoke `canonfig sync --apply --no-input`; Canonfig does not require
138
+ a resident follower daemon.
139
+
140
+ ## Security model
141
+
142
+ - There is exactly one Source Machine. Followers never publish upstream state.
143
+ - Profile Revisions are immutable, content-addressed, signed, and verified after
144
+ download.
145
+ - Enrollment pins independent TLS and source-signing fingerprints. Later
146
+ synchronization rejects changed pins, invalid signatures, digest mismatches,
147
+ replayed invitations, and revoked credentials.
148
+ - Every follower has its own revocable identity and group assignments.
149
+ - Credentials remain local references backed by the platform provider. Canonfig
150
+ does not copy Source Machine credentials into profiles or followers.
151
+ - Agent Tasks use intersecting task and harness bounds for paths, executables,
152
+ HTTPS origins, elevation, login, restart, reboot, time, input, and output.
153
+ - Canonfig captures and redacts evidence, then performs independent verification.
154
+ - Missing login, secure storage, approval, or another human-only capability
155
+ produces **Human Action Required** with exact recovery instructions.
156
+ - A modified managed skill produces **Follower Drift** and remains untouched.
157
+
158
+ Keep invitations, tokens, passwords, private keys, source signing material,
159
+ follower credentials, and SQLite state out of repositories and command examples.
160
+
161
+ ## Verified CLI examples
162
+
163
+ Plan, apply, inspect, and diagnose:
164
+
165
+ ```bash
166
+ canonfig sync --plan
167
+ canonfig sync --apply --no-input --json
168
+ canonfig status --json
169
+ canonfig doctor --no-input --timeout-ms 5000 --json
170
+ ```
171
+
172
+ Inspect profiles and select one on a follower:
173
+
174
+ ```bash
175
+ canonfig profile list
176
+ canonfig profile show revision-one
177
+ canonfig profile select workstation
178
+ ```
179
+
180
+ Inspect or set agent policy and harness bounds:
181
+
182
+ ```bash
183
+ canonfig agent policy
184
+ canonfig agent policy agent-propose
185
+ canonfig agent harness
186
+ canonfig agent harness codex --executable /opt/codex --allow-path /home/operator/.canonfig --allow-leaf-executable npm --allow-origin https://registry.npmjs.org --allow-capability restart --maximum-input-bytes 4096
187
+ ```
188
+
189
+ Manage schedules and recovery:
190
+
191
+ ```bash
192
+ canonfig schedule set weekly:Mon,Wed,Fri@12:30 --timezone Europe/Paris
193
+ canonfig schedule remove
194
+ canonfig recover --no-input --json
195
+ ```
196
+
197
+ Revoke one follower identity on the Source Machine:
198
+
199
+ ```bash
200
+ canonfig source revoke follower-one
201
+ ```
202
+
203
+ Human output is the default. `--json` emits the stable `canonfig.cli/v1`
204
+ envelope. Exit codes distinguish success (`0`), internal failure (`1`), usage or
205
+ configuration (`2`), Human Action Required (`3`), conflict or drift (`4`),
206
+ authentication or revocation (`5`), transport (`6`), and verification or apply
207
+ failure (`7`).
208
+
209
+ ## Documentation
210
+
211
+ - [Architecture and authority](website/content/docs/concepts/architecture.mdx)
212
+ - [Source setup](website/content/docs/source/setup.mdx)
213
+ - [Discovery, review, and publication](website/content/docs/source/lifecycle.mdx)
214
+ - [Follower enrollment and trust](website/content/docs/followers/enrollment.mdx)
215
+ - [Profiles and apply policies](website/content/docs/profiles/resources.mdx)
216
+ - [Synchronization](website/content/docs/operations/synchronization.mdx)
217
+ - [Agent policies and harness bounds](website/content/docs/operations/agent-policies.mdx)
218
+ - [Schedules](website/content/docs/operations/schedules.mdx)
219
+ - [Drift and Human Action Required](website/content/docs/operations/drift-and-human-action.mdx)
220
+ - [Recovery](website/content/docs/operations/recovery.mdx)
221
+ - [CLI reference](website/content/docs/reference/cli.mdx)
222
+ - [Installation skill](skills/install-canonfig/SKILL.md)
223
+ - [Operations skill](skills/operate-canonfig/SKILL.md)
224
+
225
+ ## Shipped constraints
226
+
227
+ - The source transport accepts only canonical loopback hosts, including
228
+ `localhost`, and binds to `127.0.0.1` or `::1`. Remote exposure, public
229
+ binding, and port-forwarding workflows are not part of the verified CLI
230
+ contract.
231
+ - `schedule set` accepts daily and weekly calendars. The profile contract can
232
+ represent custom calendars, but the CLI does not accept cron or native
233
+ scheduler expressions.
234
+ - Source discovery scans files passed through `--file`; it is not an implicit
235
+ whole-home scan.
236
+ - Supported agent harness kinds are `codex`, `claude`, and `gemini`.
237
+ - Third-party installers, login operations, and arbitrary agent commands do not
238
+ promise full rollback. Recovery re-observes and re-verifies them.
239
+ - Canonfig is one-way. It does not provide bidirectional sync, automatic
240
+ credential transfer, silent skill conflict resolution, whole-home backup, or
241
+ a hosted fleet control plane.
242
+ - Cross-platform acceptance and release-readiness certification is C13 work and
243
+ is not claimed by this documentation milestone.
244
+
245
+ ## Development and validation
246
+
247
+ ```bash
248
+ npm ci
249
+ npm run typecheck
250
+ npm run lint
251
+ npm test
252
+ npm run build
253
+ npm run docs:validate
254
+ npm run skills:validate
255
+ npm pack --dry-run
256
+ ```
257
+
258
+ The normal lint and test workflows include documentation command checks, skill
259
+ structure checks, and representative skill scenarios.
260
+
261
+ ## License
262
+
263
+ [MIT](LICENSE)
@@ -0,0 +1,42 @@
1
+ import { Schema } from "effect";
2
+ export class InvalidAgentTaskError extends Schema.TaggedError()("InvalidAgentTaskError", {
3
+ task: Schema.String,
4
+ message: Schema.String,
5
+ }) {
6
+ }
7
+ export class UnsupportedHarnessError extends Schema.TaggedError()("UnsupportedHarnessError", { harness: Schema.String }) {
8
+ }
9
+ export class DeniedAgentCapabilityError extends Schema.TaggedError()("DeniedAgentCapabilityError", {
10
+ capability: Schema.String,
11
+ value: Schema.String,
12
+ }) {
13
+ }
14
+ export class AgentInputLimitError extends Schema.TaggedError()("AgentInputLimitError", {
15
+ actualBytes: Schema.Number,
16
+ maximumBytes: Schema.Number,
17
+ }) {
18
+ }
19
+ export class AgentExecutionTimeoutError extends Schema.TaggedError()("AgentExecutionTimeoutError", {
20
+ executable: Schema.String,
21
+ timeoutMilliseconds: Schema.Number,
22
+ }) {
23
+ }
24
+ export class AgentExecutionCancelledError extends Schema.TaggedError()("AgentExecutionCancelledError", { executable: Schema.String }) {
25
+ }
26
+ export class AgentOutputLimitError extends Schema.TaggedError()("AgentOutputLimitError", {
27
+ executable: Schema.String,
28
+ maximumBytes: Schema.Number,
29
+ }) {
30
+ }
31
+ export class AgentProcessError extends Schema.TaggedError()("AgentProcessError", {
32
+ executable: Schema.String,
33
+ message: Schema.String,
34
+ }) {
35
+ }
36
+ export class InvalidAgentResponseError extends Schema.TaggedError()("InvalidAgentResponseError", { message: Schema.String }) {
37
+ }
38
+ export class AgentVerificationError extends Schema.TaggedError()("AgentVerificationError", {
39
+ command: Schema.Array(Schema.String),
40
+ message: Schema.String,
41
+ }) {
42
+ }
@@ -0,0 +1,204 @@
1
+ import { Effect, Layer } from "effect";
2
+ import { failedVerification, AgentResolution, decodeAgentProposal, nonzeroProcessError, profileChangeProposalFromResolution, registryOriginForInvocation, registryScopesForInvocation, redactAgentTask, revalidatePipRequirementFiles, resolvedExecutableIdentity, resolveAuthorizedProposal, validateAgentTask, } from "./agent-resolution.service.js";
3
+ import { AgentExecutionCancelledError, AgentExecutionTimeoutError, AgentInputLimitError, AgentOutputLimitError, AgentProcessError, AgentVerificationError, DeniedAgentCapabilityError, InvalidAgentResponseError, InvalidAgentTaskError, UnsupportedHarnessError, } from "./agent-resolution.errors.js";
4
+ import { executeControlledProcess, redactText, } from "./controlled-executor.js";
5
+ import { adaptHarnessInvocation, extractHarnessResponse, } from "./harness-adapters.js";
6
+ const redactCaptured = (process, secrets) => ({
7
+ ...process,
8
+ executable: redactText(process.executable, secrets),
9
+ arguments: process.arguments.map((value) => redactText(value, secrets)),
10
+ stdout: redactText(process.stdout, secrets),
11
+ stderr: redactText(process.stderr, secrets),
12
+ });
13
+ const remainingTime = (deadline) => Math.max(1, deadline - Date.now());
14
+ const ensureOutputBudget = (executable, maximum, consumed) => consumed <= maximum
15
+ ? Effect.void
16
+ : Effect.fail(new AgentOutputLimitError({
17
+ executable,
18
+ maximumBytes: maximum,
19
+ }));
20
+ const runResolution = (executor, input) => Effect.gen(function* () {
21
+ yield* validateAgentTask(input.task);
22
+ const secrets = input.secrets ?? [];
23
+ const recordedTask = redactAgentTask(input.task, secrets);
24
+ switch (input.policy) {
25
+ case "deterministic-only":
26
+ return {
27
+ outcome: "deterministic-only",
28
+ task: recordedTask,
29
+ reason: input.scheduled === true
30
+ ? "scheduled deterministic-only policy requires human action"
31
+ : "deterministic-only policy does not invoke an agent",
32
+ };
33
+ case "agent-propose":
34
+ case "agent-apply":
35
+ break;
36
+ }
37
+ const deadline = Date.now() + input.task.timeLimitSeconds * 1_000;
38
+ const invocation = adaptHarnessInvocation(input.harness, input.task);
39
+ const harnessExecutable = yield* Effect.promise(() => resolvedExecutableIdentity(invocation.executable, invocation.environment, process.cwd()));
40
+ if (harnessExecutable === undefined) {
41
+ return yield* new DeniedAgentCapabilityError({
42
+ capability: "harness-executable",
43
+ value: invocation.executable,
44
+ });
45
+ }
46
+ const rawHarness = yield* executor({
47
+ executable: harnessExecutable,
48
+ arguments: invocation.arguments,
49
+ environment: invocation.environment,
50
+ standardInput: invocation.input,
51
+ timeoutMilliseconds: remainingTime(deadline),
52
+ maximumInputBytes: input.harness.maximumInputBytes,
53
+ maximumOutputBytes: input.task.outputLimitBytes,
54
+ secrets,
55
+ signal: input.signal,
56
+ });
57
+ const harness = redactCaptured(rawHarness, secrets);
58
+ if (harness.exitCode !== 0) {
59
+ return yield* nonzeroProcessError(harness.executable, harness.exitCode, harness.stderr);
60
+ }
61
+ // `outputBytes` is measured by the executor while the raw chunks are
62
+ // still available. Never derive this budget from redacted strings:
63
+ // replacing a long secret with "[REDACTED]" would otherwise let a
64
+ // multi-action run exceed its aggregate limit.
65
+ let consumed = rawHarness.outputBytes;
66
+ yield* ensureOutputBudget(harness.executable, input.task.outputLimitBytes, consumed);
67
+ const decodedProposal = yield* decodeAgentProposal(extractHarnessResponse(input.harness.harness, harness.stdout));
68
+ const authorized = yield* resolveAuthorizedProposal(decodedProposal, input.task, input.harness);
69
+ const proposal = authorized.proposal;
70
+ if (input.policy === "agent-propose") {
71
+ return {
72
+ outcome: "proposed",
73
+ task: recordedTask,
74
+ proposal,
75
+ harness,
76
+ };
77
+ }
78
+ const executions = [];
79
+ for (const action of proposal.actions) {
80
+ const workingDirectory = action.workingDirectory ?? input.task.allowedPaths[0];
81
+ if (workingDirectory === undefined) {
82
+ return yield* new DeniedAgentCapabilityError({
83
+ capability: "path",
84
+ value: "",
85
+ });
86
+ }
87
+ yield* revalidatePipRequirementFiles(action.executable, action.arguments, workingDirectory, input.task, input.harness, action.pipRequirementFiles ?? []);
88
+ const rawProcess = yield* executor({
89
+ executable: action.executable,
90
+ arguments: action.arguments,
91
+ workingDirectory,
92
+ environment: input.harness.environment,
93
+ packageRegistryOrigin: registryOriginForInvocation(action.executable, action.arguments),
94
+ packageRegistryScopes: registryScopesForInvocation(action.executable, action.arguments),
95
+ pipRequirementFiles: action.pipRequirementFiles,
96
+ timeoutMilliseconds: remainingTime(deadline),
97
+ maximumInputBytes: 0,
98
+ maximumOutputBytes: Math.min(input.task.outputLimitBytes, Math.max(0, input.task.outputLimitBytes - consumed)),
99
+ secrets,
100
+ signal: input.signal,
101
+ });
102
+ const process = redactCaptured(rawProcess, secrets);
103
+ consumed += rawProcess.outputBytes;
104
+ yield* ensureOutputBudget(process.executable, input.task.outputLimitBytes, consumed);
105
+ executions.push(process);
106
+ if (process.exitCode !== 0) {
107
+ return yield* nonzeroProcessError(process.executable, process.exitCode, process.stderr);
108
+ }
109
+ }
110
+ const [verificationExecutable = "", ...verificationArguments] = authorized.verificationCommand;
111
+ yield* revalidatePipRequirementFiles(verificationExecutable, verificationArguments, input.task.allowedPaths[0] ?? process.cwd(), input.task, input.harness, authorized.verificationPipRequirementFiles);
112
+ const rawObserved = yield* executor({
113
+ executable: verificationExecutable,
114
+ arguments: verificationArguments,
115
+ workingDirectory: input.task.allowedPaths[0],
116
+ environment: input.harness.environment,
117
+ packageRegistryOrigin: registryOriginForInvocation(verificationExecutable, verificationArguments),
118
+ packageRegistryScopes: registryScopesForInvocation(verificationExecutable, verificationArguments),
119
+ pipRequirementFiles: authorized.verificationPipRequirementFiles,
120
+ timeoutMilliseconds: remainingTime(deadline),
121
+ maximumInputBytes: 0,
122
+ maximumOutputBytes: Math.min(input.task.outputLimitBytes, Math.max(0, input.task.outputLimitBytes - consumed)),
123
+ secrets,
124
+ signal: input.signal,
125
+ });
126
+ const observed = redactCaptured(rawObserved, secrets);
127
+ consumed += rawObserved.outputBytes;
128
+ yield* ensureOutputBudget(observed.executable, input.task.outputLimitBytes, consumed);
129
+ const matched = observed.exitCode === 0
130
+ && (input.task.verification.expectContains === undefined
131
+ || observed.stdout.includes(input.task.verification.expectContains)
132
+ || observed.stderr.includes(input.task.verification.expectContains));
133
+ const verification = {
134
+ command: input.task.verification.command,
135
+ exitCode: observed.exitCode,
136
+ stdout: observed.stdout,
137
+ stderr: observed.stderr,
138
+ matched,
139
+ };
140
+ if (!matched) {
141
+ return yield* failedVerification(input.task, `independent observer did not satisfy the verification contract: ${observed.stderr}`);
142
+ }
143
+ return {
144
+ outcome: "applied",
145
+ task: recordedTask,
146
+ proposal,
147
+ harness,
148
+ executions,
149
+ verification,
150
+ };
151
+ });
152
+ const redactResolutionError = (error, secrets) => {
153
+ const clean = (value) => redactText(value, secrets);
154
+ switch (error._tag) {
155
+ case "InvalidAgentTaskError":
156
+ return new InvalidAgentTaskError({
157
+ task: clean(error.task),
158
+ message: clean(error.message),
159
+ });
160
+ case "UnsupportedHarnessError":
161
+ return new UnsupportedHarnessError({ harness: clean(error.harness) });
162
+ case "DeniedAgentCapabilityError":
163
+ return new DeniedAgentCapabilityError({
164
+ capability: clean(error.capability),
165
+ value: clean(error.value),
166
+ });
167
+ case "AgentInputLimitError":
168
+ return new AgentInputLimitError({
169
+ actualBytes: error.actualBytes,
170
+ maximumBytes: error.maximumBytes,
171
+ });
172
+ case "AgentExecutionTimeoutError":
173
+ return new AgentExecutionTimeoutError({
174
+ executable: clean(error.executable),
175
+ timeoutMilliseconds: error.timeoutMilliseconds,
176
+ });
177
+ case "AgentExecutionCancelledError":
178
+ return new AgentExecutionCancelledError({
179
+ executable: clean(error.executable),
180
+ });
181
+ case "AgentOutputLimitError":
182
+ return new AgentOutputLimitError({
183
+ executable: clean(error.executable),
184
+ maximumBytes: error.maximumBytes,
185
+ });
186
+ case "AgentProcessError":
187
+ return new AgentProcessError({
188
+ executable: clean(error.executable),
189
+ message: clean(error.message),
190
+ });
191
+ case "InvalidAgentResponseError":
192
+ return new InvalidAgentResponseError({ message: clean(error.message) });
193
+ case "AgentVerificationError":
194
+ return new AgentVerificationError({
195
+ command: error.command.map(clean),
196
+ message: clean(error.message),
197
+ });
198
+ }
199
+ };
200
+ export const makeAgentResolutionLayer = (executor) => Layer.succeed(AgentResolution, AgentResolution.of({
201
+ resolve: (input) => runResolution(executor, input).pipe(Effect.mapError((error) => redactResolutionError(error, input.secrets ?? []))),
202
+ proposeProfileChange: profileChangeProposalFromResolution,
203
+ }));
204
+ export const AgentResolutionLive = makeAgentResolutionLayer(executeControlledProcess);