forgeos 0.1.0-alpha.64 → 0.1.0-alpha.66
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/AGENTS.md +1 -1
- package/CHANGELOG.md +20 -0
- package/docs/agent-fabric.md +213 -10
- package/package.json +1 -1
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-fabric/codex-adversarial-review.ts +229 -0
- package/src/forge/agent-fabric/index.ts +4 -0
- package/src/forge/agent-fabric/local-adaptive-digest-worker.mjs +26 -0
- package/src/forge/agent-fabric/local-adaptive-harness.ts +343 -0
- package/src/forge/agent-fabric/local-adaptive-service.ts +398 -0
- package/src/forge/agent-fabric/local-adaptive-worker.ts +146 -0
- package/src/forge/agent-fabric/local-approval-window.ts +49 -7
- package/src/forge/agent-fabric/local-change-review-service.ts +374 -0
- package/src/forge/agent-fabric/local-coding-worker.ts +107 -12
- package/src/forge/agent-fabric/local-effect-broker.ts +348 -0
- package/src/forge/agent-fabric/local-evolution-approval.ts +81 -0
- package/src/forge/agent-fabric/local-evolution-profile.ts +87 -0
- package/src/forge/agent-fabric/local-evolution-registry.ts +427 -0
- package/src/forge/agent-fabric/local-evolution-service.ts +200 -0
- package/src/forge/agent-fabric/local-intelligence.ts +281 -0
- package/src/forge/agent-fabric/local-task-contract.ts +63 -2
- package/src/forge/agent-fabric/local-task-inbox.ts +346 -2
- package/src/forge/agent-fabric/local-task-server.ts +49 -7
- package/src/forge/agent-fabric/local-task-service.ts +516 -28
- package/src/forge/agent-fabric/local-verification.ts +551 -0
- package/src/forge/agent-fabric/p0b-model-adapter.ts +37 -7
- package/src/forge/agent-memory/bridge.ts +156 -53
- package/src/forge/agent-memory/mcp.ts +64 -4
- package/src/forge/agent-memory/sources/codex-hook-runner.mjs +65 -3
- package/src/forge/cli/adaptive.ts +51 -0
- package/src/forge/cli/changed.ts +13 -32
- package/src/forge/cli/commands.ts +22 -4
- package/src/forge/cli/evolution.ts +40 -0
- package/src/forge/cli/fabric.ts +87 -9
- package/src/forge/cli/main.ts +21 -0
- package/src/forge/cli/parse.ts +49 -5
- package/src/forge/cli/studio.ts +141 -77
- package/src/forge/dev-console/cycle.ts +6 -1
- package/src/forge/review/index.ts +6 -2
- package/src/forge/version.ts +1 -1
- package/src/forge/workspace/change-summary.ts +37 -7
- package/src/forge/workspace/git-summary.ts +27 -25
package/AGENTS.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.66 input=57341432921f2c43eddb033a05cc7ad25dabd9813fcf2fd64c67f74ae5f61859 content=721818a6f9a664aa898092d4aa5cd68cbbd8ad6e150c7d3ad3ef785a2e3fcf53
|
|
2
2
|
# AGENTS.md
|
|
3
3
|
|
|
4
4
|
<!-- forge-generated:start -->
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# forgeos
|
|
2
2
|
|
|
3
|
+
## 0.1.0-alpha.66
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#66](https://github.com/Stahldavid/forge/pull/66) [`472cde3`](https://github.com/Stahldavid/forge/commit/472cde3647cf055afd627460bf015ab5467ab8d5) Thanks [@Stahldavid](https://github.com/Stahldavid)! - Add a local Agent Fabric change-review flow for Codex App authored diffs. It
|
|
8
|
+
captures an exact staged, unstaged, and new-file snapshot, runs a bounded
|
|
9
|
+
read-only Codex CLI reviewer on an isolated checkout, and keeps digest-bound
|
|
10
|
+
review rounds and findings. CLI and MCP expose proposal and evidence while
|
|
11
|
+
reviewer dispatch remains an explicit CLI action.
|
|
12
|
+
|
|
13
|
+
## 0.1.0-alpha.65
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#61](https://github.com/Stahldavid/forge/pull/61) [`6fb76d7`](https://github.com/Stahldavid/forge/commit/6fb76d734257d96d1d366528848801cbd3698395) Thanks [@Stahldavid](https://github.com/Stahldavid)! - Harden the single-owner Agent Fabric CLI and MCP pilot with durable patch and
|
|
18
|
+
verification receipts, owner-controlled source-grounded memory, guarded local
|
|
19
|
+
effects, a fixed two-worker harness, and a local Evolution Registry. Improve
|
|
20
|
+
Codex hook ingestion, Studio startup, release propagation checks, and targeted
|
|
21
|
+
CI coverage.
|
|
22
|
+
|
|
3
23
|
## 0.1.0-alpha.64
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/docs/agent-fabric.md
CHANGED
|
@@ -14,15 +14,71 @@ control journal, browser-based owner review, an isolated Ollama coding worker,
|
|
|
14
14
|
and MCP proposal/status tools backed by a local owner process. It does not make
|
|
15
15
|
a production persistence or security claim. Its scope and remaining gates are
|
|
16
16
|
in [`P0B_B_LOCAL_CODING_SCOPE.md`](./architecture/agent-fabric/P0B_B_LOCAL_CODING_SCOPE.md).
|
|
17
|
+
The [single-owner acceptance matrix](./architecture/agent-fabric/LOCAL_SINGLE_OWNER_ACCEPTANCE.md)
|
|
18
|
+
separates the current local implementation from its remaining release and human
|
|
19
|
+
acceptance gates.
|
|
20
|
+
Owner-selected local memory, fixed two-process data workers, and a standalone
|
|
21
|
+
Evolution Registry have separate narrow workflows below. These do not grant
|
|
22
|
+
the Ollama coding worker new tools or executable extensions.
|
|
23
|
+
|
|
24
|
+
`LocalAdaptiveHarness.run()` is a fixed local demonstration of two permitted
|
|
25
|
+
Node processes (`inventory` and `constraints`) followed by an authoritative
|
|
26
|
+
join. Each child receives only bounded text on stdin, an empty environment,
|
|
27
|
+
and a one second wall limit. The coordinator validates each digest against its
|
|
28
|
+
own input before committing the P0a result; cancellation, timeout, or an
|
|
29
|
+
invalid report leaves the join blocked. This trusted data worker is not an
|
|
30
|
+
arbitrary coding agent or an OS security sandbox.
|
|
31
|
+
|
|
32
|
+
The single-PC CLI wraps that harness with a local owner decision and durable
|
|
33
|
+
readback. From the repository root, create a JSON file with exactly two fields,
|
|
34
|
+
for example `{"inventory":"src/a.ts","constraints":"read only"}`. Each field
|
|
35
|
+
is data of at most 256 UTF-8 bytes. Then run:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
node bin/forge.mjs fabric adaptive-propose --file input.json --json
|
|
39
|
+
node bin/forge.mjs fabric adaptive-review <run-id> --json
|
|
40
|
+
node bin/forge.mjs fabric adaptive-run <run-id> --json
|
|
41
|
+
node bin/forge.mjs fabric adaptive-status <run-id> --json
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
To narrow the two data fields through an owner-selected Evolution profile,
|
|
45
|
+
pass `--channel canary` or `--channel stable` to `adaptive-propose` after that
|
|
46
|
+
channel has a selected `local-adaptive-input-profile` version. The proposal
|
|
47
|
+
binds the immutable version ID before review. The owner window displays it,
|
|
48
|
+
and `adaptive-run` checks that the same version remains selected and loadable
|
|
49
|
+
before issuing permits. A changed or revoked selection blocks the run. The
|
|
50
|
+
profile validates labels and lengths only; it does not provide code, tools,
|
|
51
|
+
instructions, or worker behavior.
|
|
52
|
+
Profile decisions and an adaptive run share a local process lock, so a
|
|
53
|
+
promotion or revocation cannot race between profile readback and worker dispatch.
|
|
54
|
+
|
|
55
|
+
`adaptive-review` opens a loopback browser window showing both exact inputs,
|
|
56
|
+
the bound profile version when present, and their proposal digest. The approval
|
|
57
|
+
expires after five minutes and permits one run. The
|
|
58
|
+
CLI commits the owner authorization, fixed plan, child grants, and both P0a
|
|
59
|
+
permits to a separate local PGlite journal before starting either process.
|
|
60
|
+
The result record and authoritative join can be read after closing and
|
|
61
|
+
reopening the CLI. If the process dies after committing the join but before
|
|
62
|
+
saving the result record, status still reports the authoritative join but may
|
|
63
|
+
omit process IDs and child details. A run cannot be repeated; a crash after
|
|
64
|
+
dispatch is shown as uncertain unless the durable journal contains the join.
|
|
65
|
+
Cancellation, failed workers, or missing results never authorize a join.
|
|
66
|
+
Local records and the owner verifier key live under `.forge/local/agent-fabric`.
|
|
67
|
+
The local owner lock prevents concurrent mutating CLI invocations; after a
|
|
68
|
+
crash, inspect `adaptive-status` before any manual lock recovery. This is a
|
|
69
|
+
single-PC workflow, not a production security or multi-host claim.
|
|
17
70
|
|
|
18
71
|
The following remain explicitly deferred and must not be inferred from architecture notes, historical handoffs, or local experiments:
|
|
19
72
|
|
|
20
|
-
- model-selected tools, plugins or child delegation
|
|
73
|
+
- model-selected tools, plugins or child delegation (the local data workers use
|
|
74
|
+
fixed code-owned child permits only);
|
|
21
75
|
- PGlite-backed production persistence/outbox integration for Agent Fabric;
|
|
22
|
-
- consequential-effect brokers and
|
|
76
|
+
- general consequential-effect brokers and arbitrary external-system effects
|
|
77
|
+
(the local pilot has fixed patch and Docker verification receipts only);
|
|
23
78
|
- recovery epochs and integrity-unknown recovery;
|
|
24
|
-
- adaptive model routing
|
|
25
|
-
-
|
|
79
|
+
- adaptive model routing and general harness compilation beyond the fixed
|
|
80
|
+
two-process data workflow;
|
|
81
|
+
- executable plugin promotion, shared production memory, and autonomous self-evolution;
|
|
26
82
|
- production deployment or production security claims.
|
|
27
83
|
|
|
28
84
|
## P0a scope
|
|
@@ -208,45 +264,192 @@ That public entry point exposes the hardened Conductor and hardened replay funct
|
|
|
208
264
|
## Local coding pilot (experimental)
|
|
209
265
|
|
|
210
266
|
The framework checkout also exposes a bounded single-owner CLI path. Run these commands
|
|
211
|
-
from the root of a trusted Git repository with Ollama running and `
|
|
267
|
+
from the root of a trusted Git repository with Ollama running and `qwen2.5-coder:3b`
|
|
212
268
|
installed. This pilot uses no hosted API key or Codex model turn.
|
|
213
269
|
|
|
214
270
|
```text
|
|
215
271
|
node bin/forge.mjs fabric capabilities --json
|
|
216
272
|
node bin/forge.mjs fabric propose --file task.json --json
|
|
273
|
+
node bin/forge.mjs fabric memory-add --file note.json --json
|
|
274
|
+
node bin/forge.mjs fabric memory-list --file paths.json --json
|
|
275
|
+
node bin/forge.mjs fabric memory-delete <memory-id> --json
|
|
217
276
|
node bin/forge.mjs fabric status <task-id> --json
|
|
277
|
+
node bin/forge.mjs fabric evidence <task-id> --json
|
|
218
278
|
node bin/forge.mjs fabric review <task-id> --json
|
|
219
279
|
node bin/forge.mjs fabric run <task-id> --json
|
|
280
|
+
node bin/forge.mjs fabric cancel <task-id> --json
|
|
281
|
+
node bin/forge.mjs fabric reconcile <task-id> --json
|
|
282
|
+
node bin/forge.mjs fabric verify <task-id> --json
|
|
220
283
|
node bin/forge.mjs fabric review-result <task-id> --json
|
|
221
284
|
node bin/forge.mjs fabric serve --json
|
|
222
285
|
```
|
|
223
286
|
|
|
224
287
|
`task.json` is an untrusted proposal. Its required fields are `schemaVersion: 1`,
|
|
225
288
|
`repositoryId`, the full `baseCommit`, `goal`, `acceptanceCriteria`, `nonObjectives`,
|
|
226
|
-
`sourcePaths`, `writablePaths`, `requestedModelTargetId: "target:ollama:local"`,
|
|
289
|
+
`sourcePaths`, `writablePaths`, `requestedModelTargetId: "target:ollama:local"`,
|
|
290
|
+
`requestedModelId: "qwen2.5-coder:3b"`, and
|
|
227
291
|
`limits` with `maximumAttempts`, `maximumWallClockMs`, `maximumOutputTokens`,
|
|
228
292
|
`maximumContextBytes`, `maximumPatchBytes`, and a Unix millisecond `expiresAt`.
|
|
229
293
|
`review` opens a local browser window showing the exact proposal and digest; `run`
|
|
230
294
|
consumes one approved model attempt and writes a diff in an isolated Git worktree.
|
|
295
|
+
The model ID is part of that digest and appears in the owner review. Existing
|
|
296
|
+
tasks approved before model pinning cannot start a new model call; submit a fresh
|
|
297
|
+
proposal. Their saved outcomes and patches remain available for readback, while
|
|
298
|
+
the exact model for a legacy outcome is reported as unknown.
|
|
299
|
+
The service captures the allowlisted tracked source files at the exact current
|
|
300
|
+
HEAD when proposing and rechecks them before spending the approved model
|
|
301
|
+
attempt. Changed source content or HEAD blocks a stale attempt.
|
|
302
|
+
`cancel` revokes an unused owner approval so `run` cannot start it, including after
|
|
303
|
+
the owner restarts. During an active model call, it requests abort from the
|
|
304
|
+
local adapter. The returned `model_uncertain` state does not prove the provider
|
|
305
|
+
stopped; a dispatched attempt cannot be retried. Cancellation does not stop
|
|
306
|
+
patch materialization or Docker verification that has already started. Start
|
|
307
|
+
`fabric serve` before `run` when you need a second CLI process to cancel an
|
|
308
|
+
in-flight call; a one-shot `run` has no cross-process abort endpoint.
|
|
231
309
|
`review-result` shows the recorded diff for a separate owner decision. Acceptance
|
|
232
310
|
records a decision only; it does not alter the original checkout or merge code.
|
|
233
311
|
|
|
312
|
+
Private memory is opt in and local to this checkout. `memory-add` reads a JSON
|
|
313
|
+
file such as `{ "sourcePaths": ["src/example.ts"], "text": "Owner note",
|
|
314
|
+
"retentionMs": 86400000 }`; `memory-list` reads a JSON file containing only
|
|
315
|
+
`sourcePaths`. Both require an unchanged tracked source snapshot. Notes are
|
|
316
|
+
bounded to 2 KiB, retained for at most 30 days, and stored under
|
|
317
|
+
`.forge/local/agent-fabric`. `memory-delete` removes a note by its returned ID.
|
|
318
|
+
To select notes for a coding task, add `"memoryIds": ["memory:<id>"]` to
|
|
319
|
+
`task.json` using returned full IDs. The proposal digest binds those IDs; the
|
|
320
|
+
owner review displays their text and provenance. Missing, expired, deleted, or
|
|
321
|
+
source-stale notes block approval or execution. Selected notes consume the
|
|
322
|
+
existing context byte budget and are labeled `untrusted_memory` in the model
|
|
323
|
+
context. MCP task tools cannot add, list, or delete private memory.
|
|
324
|
+
|
|
325
|
+
An optional `verification` field binds an immutable local Docker image ID and
|
|
326
|
+
two to four bounded command descriptors into the proposal digest. The first
|
|
327
|
+
descriptor is `{ "kind": "git-diff-check", "timeoutMs": 5000 }`; the remaining
|
|
328
|
+
descriptors are `{ "kind": "node-test-file", "path": "pass.test.mjs",
|
|
329
|
+
"timeoutMs": 20000 }`. The owner sees these exact commands and image in the
|
|
330
|
+
approval window. The image ID must match the locally installed `node:22`
|
|
331
|
+
image with a `node@sha256` registry digest; the service rejects a proposal
|
|
332
|
+
pointing to another local image and rechecks the tag before execution.
|
|
333
|
+
Each Node test file must exist in the pinned commit and be outside the task's
|
|
334
|
+
writable paths, so the model cannot replace the test that judges its patch.
|
|
335
|
+
After `run` produces a patch, `verify` checks deterministic test paths,
|
|
336
|
+
mount encoding, Docker context, and the pinned image before recording a durable
|
|
337
|
+
intent. A dispatch barrier is recorded before container execution. The owner
|
|
338
|
+
can clear only an intent that has no dispatch barrier; a potentially started
|
|
339
|
+
container cannot be retried automatically. `git diff --check` runs with external
|
|
340
|
+
diff and filesystem monitor helpers disabled on the host; Node tests run inside
|
|
341
|
+
Docker Desktop without network, with an
|
|
342
|
+
immutable already-installed image, read-only checkout, nonroot user and resource
|
|
343
|
+
limits. Accepting a patch with an approved verification profile requires all
|
|
344
|
+
checks to pass; a failed or uncertain result can still be rejected by the owner.
|
|
345
|
+
|
|
234
346
|
The local store lives under `.forge/local/agent-fabric` and has one PGlite process
|
|
235
347
|
owner. Start `forge fabric serve` to keep that owner running while separate CLI
|
|
236
348
|
and MCP clients connect through a loopback endpoint. The endpoint token stays in
|
|
237
|
-
the local repository store and is not printed. The MCP tools `fabric_propose
|
|
238
|
-
`fabric_status` use that same owner; they cannot approve,
|
|
349
|
+
the local repository store and is not printed. The MCP tools `fabric_propose`,
|
|
350
|
+
`fabric_status`, and `fabric_evidence` use that same owner; they cannot approve,
|
|
351
|
+
run, or accept a task. `fabric_evidence` returns a digest-bound provenance summary
|
|
352
|
+
without raw model text or diff content.
|
|
239
353
|
Without a running owner, CLI commands open the store for a single operation and
|
|
240
354
|
MCP task tools report that the owner is unavailable. A crashed model attempt with
|
|
241
355
|
a committed permit and no outcome remains
|
|
242
356
|
uncertain; a repeated `run` does not spend another attempt. A committed model result
|
|
243
|
-
can be materialized after restart
|
|
357
|
+
can be materialized after restart if no patch-effect intent was issued. Patch
|
|
358
|
+
materialization records a durable intent before creating the isolated checkout.
|
|
359
|
+
A crash after that intent reports `patch_uncertain`; `run` will not reapply it.
|
|
360
|
+
`fabric reconcile` reads the checkout and diff artifact against the committed
|
|
361
|
+
model result and records a receipt only when they match exactly. It never
|
|
362
|
+
creates or rewrites the patch. The existing MCP server reports the boundary
|
|
244
363
|
through `fabric_capabilities`. The popup is
|
|
245
364
|
a cooperative same-account interaction, so it is not a security boundary against
|
|
246
365
|
an agent with unrestricted shell or UI control. The model receives only approved
|
|
247
366
|
source files and cannot run shell commands. The Git worktree confines patch
|
|
248
|
-
materialization
|
|
367
|
+
materialization; optional Node verification is container isolated. This pilot
|
|
368
|
+
does not yet broker arbitrary consequential effects or attest to sandbox escape
|
|
369
|
+
resistance against a hostile local administrator.
|
|
249
370
|
|
|
250
371
|
Maintainers can run `bun scripts/agent-fabric-local-smoke.ts` for an opt-in real
|
|
251
372
|
Ollama fixture. That script injects a synthetic test approval and confirms a diff;
|
|
252
373
|
it does not prove the human popup flow or coding quality on real projects.
|
|
374
|
+
`FORGE_FABRIC_DOCKER_SMOKE=1 bun test tests/agent-fabric/local-task-service.test.ts`
|
|
375
|
+
exercises the service, approved verification, Docker Desktop, durable readback,
|
|
376
|
+
and acceptance with a synthetic approval callback.
|
|
377
|
+
|
|
378
|
+
### Codex App implementation with adversarial review (local preview)
|
|
379
|
+
|
|
380
|
+
The Codex App may implement a change in the current checkout. Register the work
|
|
381
|
+
before editing with a JSON request such as
|
|
382
|
+
`{"objective":"Change the parser","acceptanceCriteria":["The new case is handled"],"implementer":"codex-app"}`.
|
|
383
|
+
Place the request under the ignored `.forge/local/` directory, then use:
|
|
384
|
+
|
|
385
|
+
```bash
|
|
386
|
+
node bin/forge.mjs fabric change-propose --file .forge/local/change-request.json --json
|
|
387
|
+
node bin/forge.mjs fabric change-status --task-id <change-id> --json
|
|
388
|
+
node bin/forge.mjs fabric change-review --task-id <change-id> --json
|
|
389
|
+
node bin/forge.mjs fabric change-evidence --task-id <change-id> --json
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
`change-review` is an explicit owner CLI action that starts a **new Codex CLI
|
|
393
|
+
review turn** using the machine's existing Codex login. That turn may consume
|
|
394
|
+
Codex usage. The MCP tools `fabric_change_propose`, `fabric_change_status`, and
|
|
395
|
+
`fabric_change_evidence` can register and inspect a change but cannot start a
|
|
396
|
+
review or mark it accepted. The request pins the base commit; review later
|
|
397
|
+
captures staged, unstaged, and non-ignored new files without modifying the
|
|
398
|
+
user's Git index. Local Forge state under `.forge/local` and `.forge/delta` is
|
|
399
|
+
excluded. A review is limited to 1 MiB, 100 paths, and eight rounds; binary
|
|
400
|
+
patches, symlinks, and submodules are rejected.
|
|
401
|
+
|
|
402
|
+
Each round records an immutable diff and request digest before dispatch. The
|
|
403
|
+
reviewer runs against a separate checkout of that exact diff in read-only
|
|
404
|
+
sandbox mode. A valid structured report bound to the request digest is required;
|
|
405
|
+
process exit zero alone is insufficient. `canAccept` becomes true only for a
|
|
406
|
+
passing report on the **current** diff. Editing the checkout after a pass makes
|
|
407
|
+
it `needs_review`, and the next review creates a new round. An interrupted
|
|
408
|
+
review remains uncertain and will not automatically spend another turn. An
|
|
409
|
+
inconclusive completed review may be retried explicitly. For an interrupted
|
|
410
|
+
review, inspect its evidence and create a fresh change request if another paid
|
|
411
|
+
turn is warranted; the uncertain intent remains in the original record.
|
|
412
|
+
`canAccept` is evidence for the human workflow; this preview does not intercept
|
|
413
|
+
Git merges or edits made outside the CLI. Same-account shell access can alter
|
|
414
|
+
the local ledger and is outside the pilot's security boundary. The Codex CLI
|
|
415
|
+
review protocol has fake-process tests but no paid, real-model smoke yet.
|
|
416
|
+
|
|
417
|
+
### Local Evolution Registry (single owner)
|
|
418
|
+
|
|
419
|
+
The local extension workflow pins a candidate's bytes before evaluation. A
|
|
420
|
+
manifest is a repository file with exactly these fields:
|
|
421
|
+
|
|
422
|
+
```json
|
|
423
|
+
{"schemaVersion":1,"extensionKey":"sample","artifactPath":"extensions/sample.js"}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
The artifact and manifest must be regular files inside the repository. The
|
|
427
|
+
artifact is limited to 1 MiB and the manifest to 16 KiB. Forge copies both to
|
|
428
|
+
content addressed files under `.forge/local/agent-fabric/evolution/`; edits to
|
|
429
|
+
the source files after registration do not change the registered version.
|
|
430
|
+
|
|
431
|
+
```bash
|
|
432
|
+
node bin/forge.mjs evolution register --manifest extensions/sample.json --json
|
|
433
|
+
node bin/forge.mjs evolution evaluate extension:sha256:<digest> --json
|
|
434
|
+
node bin/forge.mjs evolution status extension:sha256:<digest> --json
|
|
435
|
+
node bin/forge.mjs evolution review canary extension:sha256:<digest> --json
|
|
436
|
+
node bin/forge.mjs evolution review promote extension:sha256:<digest> --json
|
|
437
|
+
node bin/forge.mjs evolution load sample --channel stable --json
|
|
438
|
+
node bin/forge.mjs evolution review rollback extension:sha256:<older-digest> --json
|
|
439
|
+
node bin/forge.mjs evolution review revoke extension:sha256:<digest> --json
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
Evaluation is a fixed local suite that checks stored artifact integrity,
|
|
443
|
+
stored manifest integrity, and the manifest contract. It does not execute the
|
|
444
|
+
candidate. Each version evaluates once; a failed or interrupted evaluation
|
|
445
|
+
requires a new candidate version. Canary, promotion, rollback, and revocation
|
|
446
|
+
open a loopback owner review window. Rejection or timeout leaves selection
|
|
447
|
+
unchanged. Rollback can select only a previously stable version that still has
|
|
448
|
+
a passing evaluation. Revocation clears selections and blocks future loading.
|
|
449
|
+
|
|
450
|
+
The `load` command reports verified metadata. Local runtime callers can use
|
|
451
|
+
`LocalEvolutionService.loadSelected` to obtain bytes after the same channel,
|
|
452
|
+
evaluation, revocation, and digest checks. This registry does not import or
|
|
453
|
+
execute those bytes or grant them side effects. Its review window is a
|
|
454
|
+
cooperative human checkpoint; same-account shell or browser automation is
|
|
455
|
+
outside its protection boundary. No hosted model or API key is used.
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.
|
|
1
|
+
{"defaultProvider":"local","diagnostics":[],"env":{"deployEnv":"FORGE_DEPLOY_ENV","deployId":"FORGE_DEPLOY_ID","publicReleaseId":"NEXT_PUBLIC_FORGE_RELEASE_ID","releaseId":"FORGE_RELEASE_ID"},"gitSha":"unknown","optionalProviders":["local","sentry-compatible","sentry","glitchtip","bugsink","otel","custom"],"packageName":"forgeos","packageVersion":"0.1.0-alpha.66","releaseId":"forgeos@0.1.0-alpha.66+unknown","schemaVersion":"0.1.0"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @forge-generated generator=0.1.0-alpha.
|
|
1
|
+
// @forge-generated generator=0.1.0-alpha.66 input=57341432921f2c43eddb033a05cc7ad25dabd9813fcf2fd64c67f74ae5f61859 content=1c305c2f9d3a0ad52157e8b8858790d338296f1d0cc59c0d70f93c65d9c19fc0
|
|
2
2
|
export const releaseManifest = {
|
|
3
3
|
"defaultProvider": "local",
|
|
4
4
|
"diagnostics": [],
|
|
@@ -19,7 +19,7 @@ export const releaseManifest = {
|
|
|
19
19
|
"custom"
|
|
20
20
|
],
|
|
21
21
|
"packageName": "forgeos",
|
|
22
|
-
"packageVersion": "0.1.0-alpha.
|
|
23
|
-
"releaseId": "forgeos@0.1.0-alpha.
|
|
22
|
+
"packageVersion": "0.1.0-alpha.66",
|
|
23
|
+
"releaseId": "forgeos@0.1.0-alpha.66+unknown",
|
|
24
24
|
"schemaVersion": "0.1.0"
|
|
25
25
|
} as const;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { spawn, type ChildProcessWithoutNullStreams, type SpawnOptionsWithoutStdio } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { realpathSync, statSync } from "node:fs";
|
|
4
|
+
import type { Digest } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
const DIGEST_PATTERN = /^sha256:[0-9a-f]{64}$/u;
|
|
7
|
+
const MAX_TIMEOUT_MS = 10 * 60_000;
|
|
8
|
+
const MAX_OUTPUT_BYTES = 2 * 1024 * 1024;
|
|
9
|
+
const MAX_PROMPT_BYTES = 64 * 1024;
|
|
10
|
+
const MAX_FINDINGS = 100;
|
|
11
|
+
|
|
12
|
+
export interface CodexAdversarialFinding {
|
|
13
|
+
severity: "blocker" | "high" | "medium" | "low";
|
|
14
|
+
path: string;
|
|
15
|
+
line?: number;
|
|
16
|
+
title: string;
|
|
17
|
+
explanation: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface CodexAdversarialReport {
|
|
21
|
+
requestDigest: Digest;
|
|
22
|
+
verdict: "pass" | "changes_requested" | "inconclusive";
|
|
23
|
+
summary: string;
|
|
24
|
+
findings: readonly CodexAdversarialFinding[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CodexReviewUsage {
|
|
28
|
+
inputTokens: number;
|
|
29
|
+
outputTokens: number;
|
|
30
|
+
cachedInputTokens?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export type CodexAdversarialReviewResult =
|
|
34
|
+
| { state: "reported"; report: CodexAdversarialReport; outputDigest: Digest; usage?: CodexReviewUsage }
|
|
35
|
+
| { state: "inconclusive"; error: string; report?: CodexAdversarialReport; outputDigest?: Digest; usage?: CodexReviewUsage };
|
|
36
|
+
|
|
37
|
+
export interface CodexAdversarialReviewOptions {
|
|
38
|
+
/** A prepared, exact review checkout. The caller must verify its Git identity and diff before and after this call. */
|
|
39
|
+
workspaceRoot: string;
|
|
40
|
+
/** Canonical digest of the review request and exact diff, supplied by the caller. */
|
|
41
|
+
requestDigest: Digest;
|
|
42
|
+
prompt: string;
|
|
43
|
+
timeoutMs: number;
|
|
44
|
+
maxOutputBytes: number;
|
|
45
|
+
executable?: string;
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
/** Injection point for tests; production uses node:child_process.spawn. */
|
|
48
|
+
spawnProcess?: (command: string, args: string[], options: SpawnOptionsWithoutStdio) => ChildProcessWithoutNullStreams;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
52
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function exactKeys(value: Record<string, unknown>, required: readonly string[], optional: readonly string[] = []): boolean {
|
|
56
|
+
return required.every((key) => Object.hasOwn(value, key)) &&
|
|
57
|
+
Object.keys(value).every((key) => required.includes(key) || optional.includes(key));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function nonEmptyText(value: unknown, maxLength: number): value is string {
|
|
61
|
+
return typeof value === "string" && value.trim().length > 0 && value.length <= maxLength && !value.includes("\u0000");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function validFinding(value: unknown): value is CodexAdversarialFinding {
|
|
65
|
+
if (!isRecord(value) || !exactKeys(value, ["severity", "path", "title", "explanation"], ["line"])) return false;
|
|
66
|
+
if (!["blocker", "high", "medium", "low"].includes(String(value.severity))) return false;
|
|
67
|
+
if (!nonEmptyText(value.path, 512) || value.path.includes("\\") || value.path.startsWith("/") ||
|
|
68
|
+
/^[a-z]:/iu.test(value.path) || value.path.split("/").some((segment) => segment === "" || segment === "." || segment === "..")) return false;
|
|
69
|
+
if (!nonEmptyText(value.title, 500) || !nonEmptyText(value.explanation, 4_000)) return false;
|
|
70
|
+
return value.line === undefined || (Number.isSafeInteger(value.line) && Number(value.line) > 0);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function parseReport(text: string, requestDigest: Digest): CodexAdversarialReport | undefined {
|
|
74
|
+
let value: unknown;
|
|
75
|
+
try { value = JSON.parse(text); } catch { return undefined; }
|
|
76
|
+
if (!isRecord(value) || !exactKeys(value, ["requestDigest", "verdict", "summary", "findings"])) return undefined;
|
|
77
|
+
if (value.requestDigest !== requestDigest || !["pass", "changes_requested", "inconclusive"].includes(String(value.verdict)) ||
|
|
78
|
+
!nonEmptyText(value.summary, 4_000) || !Array.isArray(value.findings) ||
|
|
79
|
+
value.findings.length > MAX_FINDINGS || !value.findings.every(validFinding)) return undefined;
|
|
80
|
+
if ((value.verdict === "pass" && value.findings.length !== 0) ||
|
|
81
|
+
(value.verdict === "changes_requested" && value.findings.length === 0)) return undefined;
|
|
82
|
+
return value as unknown as CodexAdversarialReport;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseUsage(value: unknown): CodexReviewUsage | undefined {
|
|
86
|
+
if (!isRecord(value)) return undefined;
|
|
87
|
+
const input = value.input_tokens;
|
|
88
|
+
const output = value.output_tokens;
|
|
89
|
+
const cached = value.cached_input_tokens;
|
|
90
|
+
if (!Number.isSafeInteger(input) || Number(input) < 0 ||
|
|
91
|
+
!Number.isSafeInteger(output) || Number(output) < 0) return undefined;
|
|
92
|
+
return {
|
|
93
|
+
inputTokens: Number(input), outputTokens: Number(output),
|
|
94
|
+
...(Number.isSafeInteger(cached) && Number(cached) >= 0 ? { cachedInputTokens: Number(cached) } : {}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseCodexEvents(stdout: string, requestDigest: Digest):
|
|
99
|
+
{ report?: CodexAdversarialReport; usage?: CodexReviewUsage; error?: string } {
|
|
100
|
+
let finalText: string | undefined;
|
|
101
|
+
let usage: CodexReviewUsage | undefined;
|
|
102
|
+
let completed = false;
|
|
103
|
+
let failure = false;
|
|
104
|
+
for (const line of stdout.split(/\r?\n/u)) {
|
|
105
|
+
if (!line) continue;
|
|
106
|
+
let event: unknown;
|
|
107
|
+
try { event = JSON.parse(line); } catch { return { error: "invalid_jsonl" }; }
|
|
108
|
+
if (!isRecord(event) || typeof event.type !== "string") return { error: "invalid_event" };
|
|
109
|
+
if (event.type === "item.completed" && isRecord(event.item) && event.item.type === "agent_message") {
|
|
110
|
+
if (typeof event.item.text !== "string") return { error: "invalid_agent_message" };
|
|
111
|
+
finalText = event.item.text;
|
|
112
|
+
}
|
|
113
|
+
if (event.type === "turn.completed") {
|
|
114
|
+
completed = true;
|
|
115
|
+
usage = parseUsage(event.usage);
|
|
116
|
+
}
|
|
117
|
+
if (event.type === "turn.failed" || event.type === "error") failure = true;
|
|
118
|
+
}
|
|
119
|
+
if (failure) return { error: "codex_turn_failed", usage };
|
|
120
|
+
if (!completed) return { error: "missing_completed_turn", usage };
|
|
121
|
+
if (finalText === undefined) return { error: "missing_agent_report", usage };
|
|
122
|
+
const report = parseReport(finalText, requestDigest);
|
|
123
|
+
if (!report) return { error: "invalid_agent_report", usage };
|
|
124
|
+
return { report, usage };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function digestOutput(stdout: Buffer, stderr: Buffer): Digest {
|
|
128
|
+
const hash = createHash("sha256");
|
|
129
|
+
hash.update(stdout);
|
|
130
|
+
hash.update(stderr);
|
|
131
|
+
return `sha256:${hash.digest("hex")}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Codex's read-only sandbox is defense in depth; the caller owns checkout isolation and exact-diff verification. */
|
|
135
|
+
export async function runCodexAdversarialReview(options: CodexAdversarialReviewOptions): Promise<CodexAdversarialReviewResult> {
|
|
136
|
+
if (!DIGEST_PATTERN.test(options.requestDigest) || !nonEmptyText(options.prompt, MAX_PROMPT_BYTES) ||
|
|
137
|
+
Buffer.byteLength(options.prompt, "utf8") > MAX_PROMPT_BYTES ||
|
|
138
|
+
!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs < 1 || options.timeoutMs > MAX_TIMEOUT_MS ||
|
|
139
|
+
!Number.isSafeInteger(options.maxOutputBytes) || options.maxOutputBytes < 1 || options.maxOutputBytes > MAX_OUTPUT_BYTES) {
|
|
140
|
+
throw new TypeError("Invalid Codex review bounds or request");
|
|
141
|
+
}
|
|
142
|
+
const workspaceRoot = realpathSync(options.workspaceRoot);
|
|
143
|
+
if (!statSync(workspaceRoot).isDirectory()) throw new TypeError("Codex review workspace must be a directory");
|
|
144
|
+
if (options.signal?.aborted) return { state: "inconclusive", error: "cancelled" };
|
|
145
|
+
|
|
146
|
+
const args = ["--sandbox", "read-only", "--ask-for-approval", "never", "exec", "review",
|
|
147
|
+
"--uncommitted", "--json", "--ephemeral", "-"];
|
|
148
|
+
const prompt = `${options.prompt.trimEnd()}\n\nReturn ONLY a JSON object as the final answer with exactly these keys: ` +
|
|
149
|
+
`requestDigest, verdict, summary, findings. Set requestDigest to ${options.requestDigest}. ` +
|
|
150
|
+
`verdict is pass, changes_requested, or inconclusive. Each finding has severity ` +
|
|
151
|
+
`(blocker, high, medium, low), repository-relative path, optional positive line, title, and explanation. ` +
|
|
152
|
+
`Use an empty findings array only for pass or inconclusive. Do not use Markdown fences.\n`;
|
|
153
|
+
|
|
154
|
+
return await new Promise((resolve) => {
|
|
155
|
+
let child: ChildProcessWithoutNullStreams;
|
|
156
|
+
try {
|
|
157
|
+
child = (options.spawnProcess ?? spawn)(options.executable ?? "codex", args, {
|
|
158
|
+
cwd: workspaceRoot, shell: false, windowsHide: true, stdio: ["pipe", "pipe", "pipe"],
|
|
159
|
+
});
|
|
160
|
+
} catch {
|
|
161
|
+
resolve({ state: "inconclusive", error: "spawn_failed" });
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const stdout: Buffer[] = [];
|
|
166
|
+
const stderr: Buffer[] = [];
|
|
167
|
+
let captured = 0;
|
|
168
|
+
let reason: string | undefined;
|
|
169
|
+
let settled = false;
|
|
170
|
+
let hardTimer: ReturnType<typeof setTimeout> | undefined;
|
|
171
|
+
const finish = (code: number | null) => {
|
|
172
|
+
if (settled) return;
|
|
173
|
+
settled = true;
|
|
174
|
+
clearTimeout(timer);
|
|
175
|
+
if (hardTimer) clearTimeout(hardTimer);
|
|
176
|
+
options.signal?.removeEventListener("abort", cancel);
|
|
177
|
+
const out = Buffer.concat(stdout);
|
|
178
|
+
const err = Buffer.concat(stderr);
|
|
179
|
+
const outputDigest = digestOutput(out, err);
|
|
180
|
+
if (reason) {
|
|
181
|
+
resolve({ state: "inconclusive", error: reason, outputDigest });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (code !== 0) {
|
|
185
|
+
resolve({ state: "inconclusive", error: "codex_nonzero_exit", outputDigest });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const parsed = parseCodexEvents(out.toString("utf8"), options.requestDigest);
|
|
189
|
+
if (!parsed.report) {
|
|
190
|
+
resolve({ state: "inconclusive", error: parsed.error ?? "invalid_agent_report", outputDigest,
|
|
191
|
+
...(parsed.usage ? { usage: parsed.usage } : {}) });
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (parsed.report.verdict === "inconclusive") {
|
|
195
|
+
resolve({ state: "inconclusive", error: "reviewer_inconclusive", report: parsed.report, outputDigest,
|
|
196
|
+
...(parsed.usage ? { usage: parsed.usage } : {}) });
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
resolve({ state: "reported", report: parsed.report, outputDigest,
|
|
200
|
+
...(parsed.usage ? { usage: parsed.usage } : {}) });
|
|
201
|
+
};
|
|
202
|
+
const stop = (why: string) => {
|
|
203
|
+
if (reason || settled) return;
|
|
204
|
+
reason = why;
|
|
205
|
+
child.kill();
|
|
206
|
+
hardTimer = setTimeout(() => finish(null), 2_000);
|
|
207
|
+
};
|
|
208
|
+
const capture = (chunk: Buffer, target: Buffer[]) => {
|
|
209
|
+
if (reason || settled) return;
|
|
210
|
+
const remaining = options.maxOutputBytes - captured;
|
|
211
|
+
if (remaining > 0) {
|
|
212
|
+
const part = chunk.subarray(0, remaining);
|
|
213
|
+
target.push(part);
|
|
214
|
+
captured += part.length;
|
|
215
|
+
}
|
|
216
|
+
if (chunk.length > remaining) stop("output_limit");
|
|
217
|
+
};
|
|
218
|
+
const cancel = () => stop("cancelled");
|
|
219
|
+
child.stdout.on("data", (chunk: Buffer) => capture(chunk, stdout));
|
|
220
|
+
child.stderr.on("data", (chunk: Buffer) => capture(chunk, stderr));
|
|
221
|
+
child.on("error", () => stop("spawn_failed"));
|
|
222
|
+
child.on("close", finish);
|
|
223
|
+
const timer = setTimeout(() => stop("timeout"), options.timeoutMs);
|
|
224
|
+
options.signal?.addEventListener("abort", cancel, { once: true });
|
|
225
|
+
if (options.signal?.aborted) cancel();
|
|
226
|
+
child.stdin.on("error", () => stop("stdin_failed"));
|
|
227
|
+
try { child.stdin.end(prompt); } catch { stop("stdin_failed"); }
|
|
228
|
+
});
|
|
229
|
+
}
|
|
@@ -8,6 +8,10 @@ export type {
|
|
|
8
8
|
} from "./hardened-conductor.ts";
|
|
9
9
|
export * from "./errors.ts";
|
|
10
10
|
export * from "./journal.ts";
|
|
11
|
+
export { LocalEvolutionRegistry } from "./local-evolution-registry.ts";
|
|
12
|
+
export type { ExtensionCandidate, ExtensionVersion, FixedEvaluationSuite, EvaluationRecord,
|
|
13
|
+
EvolutionChannel, EvolutionDecisionAction, EvolutionVersionStatus, EvolutionOwnerVerifier } from "./local-evolution-registry.ts";
|
|
14
|
+
export { LocalEvolutionService, LOCAL_EVOLUTION_SUITE } from "./local-evolution-service.ts";
|
|
11
15
|
export * from "./p0a.ts";
|
|
12
16
|
export * from "./p0b-model-adapter.ts";
|
|
13
17
|
export * from "./planning.ts";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Fixed data-only worker. This file deliberately has no Forge imports, tools,
|
|
2
|
+
// filesystem access, or network calls. The coordinator verifies its output.
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
|
|
5
|
+
let input = "";
|
|
6
|
+
process.stdin.setEncoding("utf8");
|
|
7
|
+
process.stdin.on("data", (chunk) => {
|
|
8
|
+
input += chunk;
|
|
9
|
+
if (Buffer.byteLength(input, "utf8") > 1_024) process.exit(2);
|
|
10
|
+
});
|
|
11
|
+
process.stdin.on("end", () => {
|
|
12
|
+
try {
|
|
13
|
+
const message = JSON.parse(input);
|
|
14
|
+
if (!message || Object.keys(message).sort().join(",") !== "input,role" ||
|
|
15
|
+
!["inventory", "constraints"].includes(message.role) ||
|
|
16
|
+
typeof message.input !== "string" || message.input.length > 256) {
|
|
17
|
+
process.exitCode = 2;
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const canonical = JSON.stringify({ input: message.input, role: message.role });
|
|
21
|
+
const resultDigest = `sha256:${createHash("sha256").update(canonical).digest("hex")}`;
|
|
22
|
+
process.stdout.write(JSON.stringify({ pid: process.pid, role: message.role, resultDigest }));
|
|
23
|
+
} catch {
|
|
24
|
+
process.exitCode = 2;
|
|
25
|
+
}
|
|
26
|
+
});
|