@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.
- package/LICENSE +21 -0
- package/README.md +263 -0
- package/dist/agent/agent-resolution.errors.js +42 -0
- package/dist/agent/agent-resolution.layer.js +204 -0
- package/dist/agent/agent-resolution.service.js +2259 -0
- package/dist/agent/agent-resolution.types.js +1 -0
- package/dist/agent/controlled-executor.js +704 -0
- package/dist/agent/harness-adapters.js +85 -0
- package/dist/cli/cli.js +618 -0
- package/dist/cli/exit-codes.js +28 -0
- package/dist/cli/follower-commands.js +3 -0
- package/dist/cli/render.js +56 -0
- package/dist/cli/source-commands.js +5 -0
- package/dist/domain/brand.js +29 -0
- package/dist/domain/identity.js +31 -0
- package/dist/domain/npm-package-spec.js +186 -0
- package/dist/domain/profile.js +950 -0
- package/dist/domain/recipe-versions.js +297 -0
- package/dist/domain/resource.js +259 -0
- package/dist/domain/synchronization.js +346 -0
- package/dist/enrollment/enrollment.errors.js +43 -0
- package/dist/enrollment/enrollment.layer.js +724 -0
- package/dist/enrollment/enrollment.service.js +3 -0
- package/dist/enrollment/enrollment.types.js +59 -0
- package/dist/enrollment/follower-client.js +585 -0
- package/dist/enrollment/source-server.js +313 -0
- package/dist/machine/linux.layer.js +1183 -0
- package/dist/machine/machine-state.errors.js +52 -0
- package/dist/machine/machine-state.service.js +3 -0
- package/dist/machine/machine-state.types.js +1 -0
- package/dist/machine/macos.layer.js +470 -0
- package/dist/machine/windows.layer.js +879 -0
- package/dist/profile/discovery.js +740 -0
- package/dist/profile/profile-catalog.errors.js +50 -0
- package/dist/profile/profile-catalog.layer.js +20 -0
- package/dist/profile/profile-catalog.service.js +7 -0
- package/dist/profile/profile-codec.js +153 -0
- package/dist/profile/publication.js +298 -0
- package/dist/profile/tool-catalog.js +384 -0
- package/dist/runtime/doctor.js +306 -0
- package/dist/runtime/layers.js +706 -0
- package/dist/runtime/main.js +38 -0
- package/dist/schedule/linux-schedule.js +24 -0
- package/dist/schedule/macos-schedule.js +25 -0
- package/dist/schedule/schedule-manager.errors.js +17 -0
- package/dist/schedule/schedule-manager.layer.js +205 -0
- package/dist/schedule/schedule-manager.service.js +3 -0
- package/dist/schedule/schedule-manager.types.js +114 -0
- package/dist/schedule/windows-schedule.js +25 -0
- package/dist/state/state-repository.errors.js +55 -0
- package/dist/state/state-repository.layer.js +1507 -0
- package/dist/state/state-repository.service.js +3 -0
- package/dist/state/state-repository.types.js +1 -0
- package/dist/state/state-schema.js +298 -0
- package/dist/synchronization/config-codec.js +97 -0
- package/dist/synchronization/executor.js +700 -0
- package/dist/synchronization/follower-orchestration.js +939 -0
- package/dist/synchronization/follower-sync-config.js +81 -0
- package/dist/synchronization/npm-artifact.js +670 -0
- package/dist/synchronization/planner.js +378 -0
- package/dist/synchronization/recovery.js +397 -0
- package/dist/synchronization/resource-executors.js +1198 -0
- package/dist/synchronization/resource-plans.js +645 -0
- package/dist/synchronization/synchronization.errors.js +102 -0
- package/dist/synchronization/synchronization.layer.js +97 -0
- package/dist/synchronization/synchronization.service.js +11 -0
- package/dist/synchronization/synchronization.types.js +1 -0
- 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);
|