@cassiomc1/forgeloop 1.12.0 → 1.14.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/.github/copilot-instructions.md +1 -1
- package/AGENTS.md +1 -1
- package/AGENT_COMPATIBILITY.md +8 -0
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +90 -0
- package/DOCS_INDEX.md +46 -12
- package/ENG/c-development-eng.md +112 -0
- package/ENG/cpp-development-eng.md +109 -0
- package/ENG/dotnet-aspnetcore-development-eng.md +401 -0
- package/ENG/go-development-eng.md +103 -0
- package/ENG/java-development-eng.md +125 -0
- package/ENG/nodejs-backend-development-eng.md +605 -0
- package/ENG/php-development-eng.md +104 -0
- package/ENG/rust-development-eng.md +422 -0
- package/ENG/sec-code-eng.md +7 -7
- package/ENG/sql-development-eng.md +108 -0
- package/ENG/swift-development-eng.md +111 -0
- package/ENG/typescript-development-eng.md +108 -0
- package/EXECUTION_STATE.md +12 -0
- package/GUIDE_ROUTER.md +418 -9
- package/LOOP_ENGINEERING.md +28 -2
- package/ORCHESTRATOR_INTEGRATION.md +9 -5
- package/PROTOCOL_INTEGRATION.md +55 -2
- package/QUALITY_SCORECARD.md +1 -0
- package/README.md +78 -52
- package/TERMINOLOGY.md +2 -0
- package/THIRD_PARTY_NOTICES.md +19 -7
- package/THREAT_MODEL.md +140 -1
- package/completions/_forgeloop +22 -4
- package/completions/forgeloop.bash +40 -4
- package/completions/forgeloop.fish +130 -1
- package/docs/ADVISORY_CONTEXT.md +25 -0
- package/docs/AGENT_BROWSER_ADAPTER.md +81 -0
- package/docs/AGENT_BROWSER_VERIFICATION.md +6 -0
- package/docs/AGENT_PROTOCOL_SUMMARY.md +81 -3
- package/docs/AGENT_SKILL.md +66 -0
- package/docs/ARTIFACT_REFERENCE.md +123 -0
- package/docs/AUDIT_UX.md +46 -0
- package/docs/BROWSER_VERIFICATION.md +136 -0
- package/docs/CLI_REFERENCE.md +392 -10
- package/docs/CODE_ATTESTATION.md +2 -2
- package/docs/DOCUMENTATION_GUIDE.md +34 -12
- package/docs/GETTING_STARTED.md +59 -0
- package/docs/JEV_BENCHMARKS.md +31 -0
- package/docs/MODEL_ROUTING.md +37 -0
- package/docs/OPENSRC_ADAPTER.md +241 -0
- package/docs/PACKAGE_CONTENTS.md +60 -19
- package/docs/PROVIDERS.md +126 -0
- package/docs/PROVIDER_ARCHITECTURE.md +199 -0
- package/docs/RECIPES.md +32 -0
- package/docs/RELEASE_CHECKLIST.md +66 -5
- package/docs/SECURITY_REVIEW.md +71 -0
- package/docs/SEMANTIC_DECISION_PLANE.md +71 -0
- package/docs/TEST_INTELLIGENCE.md +29 -0
- package/docs/TEST_PRUNING.md +14 -0
- package/docs/TROUBLESHOOTING.md +298 -3
- package/docs/UNIVERSAL_INTEGRATION.md +31 -0
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.html +2 -2
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-code-attestation-flow.svg +1 -1
- package/docs/assets/diagrams/forgeloop-engineering-flow.html +39 -26
- package/docs/assets/diagrams/forgeloop-engineering-flow.receipt.json +6 -6
- package/docs/assets/diagrams/forgeloop-engineering-flow.svg +26 -26
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.html +2 -1
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.receipt.json +5 -5
- package/docs/assets/diagrams/forgeloop-verification-trust-flow.svg +1 -1
- package/docs/diagrams/README.md +13 -9
- package/docs/diagrams/forgeloop-code-attestation-flow.workflow.json +1 -1
- package/docs/diagrams/forgeloop-engineering-flow.workflow.json +24 -19
- package/docs/diagrams/forgeloop-verification-trust-flow.workflow.json +1 -0
- package/docs/diagrams/reviews/forgeloop-code-attestation-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-engineering-flow.review.json +4 -4
- package/docs/diagrams/reviews/forgeloop-verification-trust-flow.review.json +4 -4
- package/docs/documentation-manifest.json +1397 -0
- package/docs/protocol-requirements.json +101 -0
- package/package.json +46 -4
- package/schemas/config.schema.json +14 -0
- package/schemas/context-plan.schema.json +18 -0
- package/schemas/routing-input.schema.json +1 -1
- package/schemas/semantic-decision.schema.json +46 -0
- package/schemas/test-utility.schema.json +44 -0
- package/scripts/CI_VALIDATORS.md +84 -11
- package/scripts/benchmark-jev.mjs +5 -0
- package/scripts/benchmark-test-intelligence.mjs +4 -0
- package/scripts/generate-agent-protocol-summary.mjs +40 -1
- package/scripts/generate-forgeloop-skill.mjs +133 -0
- package/scripts/jev-smoke.mjs +19 -0
- package/skills/forgeloop/README.md +9 -0
- package/skills/forgeloop/SKILL.md +77 -0
- package/skills/forgeloop/references/lifecycle.md +9 -0
- package/skills/forgeloop/references/recovery.md +7 -0
- package/skills/forgeloop/references/verification.md +7 -0
- package/src/adapters/agent-browser/assertions.js +47 -0
- package/src/adapters/agent-browser/commands.js +54 -0
- package/src/adapters/agent-browser/index.js +3 -0
- package/src/adapters/agent-browser/locator.js +40 -0
- package/src/adapters/agent-browser/process.js +215 -0
- package/src/adapters/agent-browser/provider.js +313 -0
- package/src/adapters/emulated-services/constants.js +24 -0
- package/src/adapters/emulated-services/index.js +7 -0
- package/src/adapters/emulated-services/process.js +162 -0
- package/src/adapters/emulated-services/provider.js +282 -0
- package/src/adapters/opensrc/normalize.js +90 -0
- package/src/adapters/opensrc/process.js +248 -0
- package/src/adapters/opensrc/provider.js +338 -0
- package/src/adapters/opensrc/search.js +264 -0
- package/src/adapters/typesafe/client.js +28 -0
- package/src/adapters/typesafe/engine.js +63 -0
- package/src/adapters/typesafe/normalize.js +41 -0
- package/src/cli.js +108 -0
- package/src/commands/checkpoint-revalidate.js +176 -0
- package/src/commands/context-plan.js +38 -0
- package/src/commands/contract-create.js +264 -0
- package/src/commands/contract-revise.js +236 -0
- package/src/commands/decision-show.js +14 -0
- package/src/commands/decision-status.js +22 -0
- package/src/commands/discover.js +41 -0
- package/src/commands/doctor.js +15 -0
- package/src/commands/gate-record.js +205 -0
- package/src/commands/gate-revalidate.js +137 -0
- package/src/commands/model-route.js +32 -0
- package/src/commands/next.js +19 -7
- package/src/commands/route.js +146 -18
- package/src/commands/semantic-plan.js +17 -0
- package/src/commands/task-abandon.js +224 -0
- package/src/commands/task-create.js +84 -25
- package/src/commands/task-list.js +22 -2
- package/src/commands/task-migrate-contract-bootstrap-repair.js +288 -0
- package/src/commands/task-repair-contract-bootstrap.js +263 -0
- package/src/commands/test-inventory.js +5 -0
- package/src/commands/test-prune-plan.js +5 -0
- package/src/commands/test-prune-probe.js +5 -0
- package/src/commands/test-utility.js +5 -0
- package/src/commands/validate-protocol.js +10 -1
- package/src/config/guides.json +44 -0
- package/src/core/artifact-registry.js +24 -0
- package/src/core/audit-ux.js +514 -0
- package/src/core/browser-verification/constants.js +149 -0
- package/src/core/browser-verification/normalize.js +254 -0
- package/src/core/browser-verification/provider.js +519 -0
- package/src/core/browser-verification/service.js +115 -0
- package/src/core/build-script.js +151 -0
- package/src/core/c-cpp-project.js +143 -0
- package/src/core/checkpoint-revalidation.js +319 -0
- package/src/core/cli-command-definitions.js +249 -1
- package/src/core/command-executors.js +115 -3
- package/src/core/command-input.js +212 -102
- package/src/core/completion-artifacts.js +14 -5
- package/src/core/completion.js +4 -6
- package/src/core/config.js +3 -0
- package/src/core/context-compiler/budget.js +9 -0
- package/src/core/context-compiler/candidates.js +39 -0
- package/src/core/context-compiler/compiler.js +63 -0
- package/src/core/context-compiler/fingerprint.js +11 -0
- package/src/core/context-compiler/policy.js +13 -0
- package/src/core/context-compiler/result.js +23 -0
- package/src/core/contract-bootstrap-recovery.js +655 -0
- package/src/core/contract-presets.js +82 -0
- package/src/core/contract-revision.js +210 -0
- package/src/core/decision/artifact.js +69 -0
- package/src/core/decision/benchmarks.js +103 -0
- package/src/core/decision/cache.js +27 -0
- package/src/core/decision/constants.js +58 -0
- package/src/core/decision/cutover.js +34 -0
- package/src/core/decision/engine.js +22 -0
- package/src/core/decision/errors.js +68 -0
- package/src/core/decision/events.js +101 -0
- package/src/core/decision/freshness.js +19 -0
- package/src/core/decision/normalizers/index.js +115 -0
- package/src/core/decision/policy.js +18 -0
- package/src/core/decision/projection.js +16 -0
- package/src/core/decision/question-registry.js +201 -0
- package/src/core/decision/request.js +26 -0
- package/src/core/decision/resolver.js +130 -0
- package/src/core/decision/result.js +58 -0
- package/src/core/decision/service.js +156 -0
- package/src/core/decision/state-builder.js +65 -0
- package/src/core/decision/task-bindings.js +30 -0
- package/src/core/decision/test-provider.js +32 -0
- package/src/core/decision/thresholds.js +15 -0
- package/src/core/error-codes.js +281 -3
- package/src/core/events.js +226 -57
- package/src/core/evidence-readiness.js +9 -0
- package/src/core/execution-prerequisites.js +14 -0
- package/src/core/execution-profile.js +63 -38
- package/src/core/filesystem.js +1 -10
- package/src/core/gate-provenance.js +124 -0
- package/src/core/go-project.js +206 -0
- package/src/core/integration-invocation-policy.js +27 -4
- package/src/core/integration-resources.js +86 -61
- package/src/core/java-project.js +403 -0
- package/src/core/model-router/constants.js +10 -0
- package/src/core/model-router/policy.js +103 -0
- package/src/core/model-router/router.js +37 -0
- package/src/core/multi-language-project.js +117 -0
- package/src/core/next-action-model.js +58 -0
- package/src/core/next-action-phases.js +130 -42
- package/src/core/next-action-refresh.js +43 -9
- package/src/core/next-action-review-phase.js +7 -2
- package/src/core/next-action.js +35 -7
- package/src/core/next-explanation.js +63 -0
- package/src/core/phase.js +128 -10
- package/src/core/php-project.js +85 -0
- package/src/core/preflight-consistency.js +23 -9
- package/src/core/preflight-loaders.js +37 -5
- package/src/core/project-detection.js +1760 -52
- package/src/core/protocol-info.js +65 -0
- package/src/core/protocol.js +20 -0
- package/src/core/reconcile-closure.js +132 -53
- package/src/core/recovery-history.js +1 -0
- package/src/core/resumability.js +154 -44
- package/src/core/route-artifact.js +15 -1
- package/src/core/router.js +223 -4
- package/src/core/runtime-context.js +118 -61
- package/src/core/rust-project.js +400 -0
- package/src/core/schema-validation.js +3 -0
- package/src/core/security-review/constants.js +64 -0
- package/src/core/security-review/normalize.js +245 -0
- package/src/core/security-review/provider.js +204 -0
- package/src/core/security-review/service.js +134 -0
- package/src/core/semantic-planning/constants.js +19 -0
- package/src/core/semantic-planning/projection.js +94 -0
- package/src/core/semantic-planning/service.js +15 -0
- package/src/core/sources.js +37 -0
- package/src/core/sql-project.js +141 -0
- package/src/core/swift-project.js +200 -0
- package/src/core/task-claim-state.js +201 -1
- package/src/core/task-conflict-inspection.js +31 -5
- package/src/core/task-paths.js +13 -0
- package/src/core/task-recovery.js +1 -0
- package/src/core/templates.js +3 -0
- package/src/core/test-intelligence/benchmarks.js +68 -0
- package/src/core/test-intelligence/inventory.js +73 -0
- package/src/core/test-intelligence/prune.js +90 -0
- package/src/core/test-intelligence/semantic-state.js +15 -0
- package/src/core/test-intelligence/service.js +40 -0
- package/src/core/test-intelligence/utility.js +50 -0
- package/src/core/trace.js +11 -7
- package/src/core/transaction.js +1 -0
- package/src/core/typescript-project.js +349 -0
- package/src/core/xml-structure.js +123 -0
- package/src/integration.d.ts +492 -0
- package/src/integration.js +54 -0
- package/src/providers/README.md +47 -0
- package/src/providers/capabilities.js +46 -0
- package/src/providers/errors.js +15 -0
- package/src/providers/index.js +29 -0
- package/src/providers/json-snapshot.js +105 -0
- package/src/providers/registry.js +152 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFile, mkdir, writeFile } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import { protocolInfo } from "../src/core/protocol-info.js";
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
|
+
const summaryPath = path.join(root, "docs", "AGENT_PROTOCOL_SUMMARY.md");
|
|
11
|
+
const outputRoot = path.join(root, "skills", "forgeloop");
|
|
12
|
+
const marker = "<!-- Generated by scripts/generate-forgeloop-skill.mjs. Do not edit manually. -->";
|
|
13
|
+
|
|
14
|
+
function skillContent(info, summary) {
|
|
15
|
+
const phases = info.lifecycle.phases.join(", ");
|
|
16
|
+
return `---
|
|
17
|
+
name: forgeloop
|
|
18
|
+
description: >-
|
|
19
|
+
Operate a repository governed by the ForgeLoop protocol: discover or resume
|
|
20
|
+
tasks, follow canonical next actions, preserve claims and continuity, record
|
|
21
|
+
verification evidence, recover supported protocol state, and require
|
|
22
|
+
validator-backed completion.
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
${marker}
|
|
26
|
+
|
|
27
|
+
# ForgeLoop
|
|
28
|
+
|
|
29
|
+
## Purpose
|
|
30
|
+
|
|
31
|
+
This skill is operational guidance generated from ForgeLoop's canonical protocol metadata. It is not protocol authority. Always follow the current repository runtime, \`protocol-info\`, \`next\`, schemas, validators, and canonical documentation when they differ from this skill.
|
|
32
|
+
|
|
33
|
+
## When to Use
|
|
34
|
+
|
|
35
|
+
Use when ForgeLoop is present in the repository or explicitly requested. Do not activate it for unrelated repositories merely because an agent supports skills.
|
|
36
|
+
|
|
37
|
+
## Source of Truth
|
|
38
|
+
|
|
39
|
+
Runtime registries and validators outrank canonical protocol docs; canonical docs outrank \`docs/AGENT_PROTOCOL_SUMMARY.md\`; this generated skill is the final non-normative projection. Protocol version is ${info.protocolVersion}. Mutable facts such as phases (${phases}) must be read from the runtime.
|
|
40
|
+
|
|
41
|
+
## Command Resolution
|
|
42
|
+
|
|
43
|
+
In ForgeLoop's own source repository, use \`node src/cli.js\`. In a consumer repository, use that repository's configured local invocation. Never silently install or upgrade ForgeLoop, and never use a stale global CLI when repository-local code exists. If compatibility is uncertain, inspect \`protocol-info --json\`.
|
|
44
|
+
|
|
45
|
+
## Operating Workflow
|
|
46
|
+
|
|
47
|
+
1. Discover existing work with \`node src/cli.js task-list --json\`.
|
|
48
|
+
2. Select or resume the relevant task; a harness, session, model, or context change does not create a new task.
|
|
49
|
+
3. Ask ForgeLoop for \`next --task <id> --json\` at every boundary and execute only the returned supported action.
|
|
50
|
+
4. Require \`preflight\` to return \`READY\` before implementation.
|
|
51
|
+
5. Preserve fresh, validator-owned evidence through verification, review, completion, and the terminal \`next\` check.
|
|
52
|
+
|
|
53
|
+
## Task Discovery and Continuation
|
|
54
|
+
|
|
55
|
+
Respect active write claims. Do not steal, override, manually release, or edit ownership artifacts. Reconcile continuity with canonical state rather than trusting stale narrative.
|
|
56
|
+
|
|
57
|
+
## Verification Rules
|
|
58
|
+
|
|
59
|
+
\`run-check\` executes the exact command and records ForgeLoop-owned provenance. \`record-check\` is serialization-only; its command text is metadata and is never executed. Claims, provider/advisory output, screenshots, and actor summaries are not evidence by themselves. Missing tools remain unavailable; do not auto-install a verifier.
|
|
60
|
+
|
|
61
|
+
## Recovery Rules
|
|
62
|
+
|
|
63
|
+
Use supported recovery only. Preserve failed evidence and follow \`next\` through diagnosis/correction. For state or ledger divergence, stop lifecycle mutation and inspect validation/classification. Never edit task state, receipts, recovery, continuity, claims, hashes, or \`events.ndjson\` manually. Reconcile \`COMMIT_UNKNOWN\` before any retry.
|
|
64
|
+
|
|
65
|
+
## Completion Rule
|
|
66
|
+
|
|
67
|
+
Never claim ForgeLoop-verified completion unless \`complete\` returns \`VALID\`. Green tests, CI, a merged PR, or provider output alone are insufficient. After \`VALID\`, run \`next --task <id> --json\` again and require the canonical terminal action.
|
|
68
|
+
|
|
69
|
+
## Cross-Harness Continuity
|
|
70
|
+
|
|
71
|
+
A harness or session change does not create a new task. Use supported continuity or handoff commands and reconcile persisted state before continuing.
|
|
72
|
+
|
|
73
|
+
## Durable Action Safety
|
|
74
|
+
|
|
75
|
+
For external actions use propose, authorize, execute, then verify or reconcile. Never blindly retry an uncertain external commit.
|
|
76
|
+
|
|
77
|
+
## Never Do
|
|
78
|
+
|
|
79
|
+
- Never manually assign COMPLETE.
|
|
80
|
+
- Never invent lifecycle chronology or override \`next\`.
|
|
81
|
+
- Never override active claims or rewrite the append-only ledger.
|
|
82
|
+
- Never fabricate evidence or treat \`record-check\` as execution.
|
|
83
|
+
- Never auto-install ForgeLoop or a verifier during normal operation.
|
|
84
|
+
- Never duplicate a task because the harness changed.
|
|
85
|
+
- Never treat provider/advisory output as protocol authority.
|
|
86
|
+
|
|
87
|
+
## References
|
|
88
|
+
|
|
89
|
+
- [Canonical Agent Protocol Summary](../../docs/AGENT_PROTOCOL_SUMMARY.md)
|
|
90
|
+
- [Lifecycle](./references/lifecycle.md)
|
|
91
|
+
- [Verification](./references/verification.md)
|
|
92
|
+
- [Recovery](./references/recovery.md)
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function referenceContent(title, body, summary) {
|
|
97
|
+
return `${marker}\n\n# ${title}\n\n${body}\n\n<!-- Canonical summary fingerprint source: docs/AGENT_PROTOCOL_SUMMARY.md -->\n${summary.includes("# ForgeLoop Agent Protocol Summary") ? "" : ""}`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function files(info, summary) {
|
|
101
|
+
return {
|
|
102
|
+
"SKILL.md": skillContent(info, summary),
|
|
103
|
+
"README.md": `${marker}\n\n# ForgeLoop Agent Skill\n\nThis directory contains a generated, portable, harness-neutral Agent Skill. It is operational guidance, not protocol authority, and has no dependency on external Agent Skills tooling.\n\nCanonical hierarchy: runtime registries and validators -> canonical protocol docs -> \`docs/AGENT_PROTOCOL_SUMMARY.md\` -> this generated projection.\n\nRegenerate with \`npm run skill:generate\`; check exact freshness with \`npm run skill:check\`. The generator is deterministic, offline, and writes only the five known files in this directory.\n`,
|
|
104
|
+
"references/lifecycle.md": referenceContent("Lifecycle", `The canonical loop is task discovery, contract, route, preflight, execution, verification, review, validator-backed completion, and a terminal next check. Always query \`next\` rather than inferring a transition. The initial post-task-create shape may have no work state: \`next\` returns DISCOVER, then CREATE_CONTRACT, and only a real contract fingerprint permits the first work-state checkpoint.\n\nPhases are runtime-owned and currently include: ${info.lifecycle.phases.join(", ")}.`, summary),
|
|
105
|
+
"references/verification.md": referenceContent("Verification", "Use run-check for exact command execution and provenance. Use record-check only for supplied observations; it never launches command text. Evidence must be fresh, task-bound, requirement-bound, and validator-readable. Preserve failures, report unavailable tools, and require observed coverage before review and complete -> VALID.", summary),
|
|
106
|
+
"references/recovery.md": referenceContent("Recovery", "Fail closed on invalid ownership, state, ledger, receipt, or continuity. Use supported recovery commands only. Never edit .forgeloop/task-state, work-state.json, events.ndjson, recovery.json, claims, or hashes by hand. Claim release is not a lifecycle bypass. Reconcile COMMIT_UNKNOWN before retrying any external action.", summary),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function main() {
|
|
111
|
+
const args = process.argv.slice(2);
|
|
112
|
+
if (args.length !== 1 || !["--write", "--check"].includes(args[0])) {
|
|
113
|
+
throw new Error("Usage: node scripts/generate-forgeloop-skill.mjs --write|--check");
|
|
114
|
+
}
|
|
115
|
+
const summary = await readFile(summaryPath, "utf8");
|
|
116
|
+
const generated = files(protocolInfo(), summary);
|
|
117
|
+
let failed = false;
|
|
118
|
+
for (const [relative, content] of Object.entries(generated)) {
|
|
119
|
+
const target = path.join(outputRoot, relative);
|
|
120
|
+
let current = null;
|
|
121
|
+
try { current = await readFile(target, "utf8"); } catch (error) { if (error.code !== "ENOENT") throw error; }
|
|
122
|
+
if (args[0] === "--check") {
|
|
123
|
+
if (current !== content) { console.error(`Generated Agent Skill is stale: skills/forgeloop/${relative}`); failed = true; }
|
|
124
|
+
} else {
|
|
125
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
126
|
+
await writeFile(target, content, "utf8");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (failed) process.exitCode = 1;
|
|
130
|
+
else console.log(args[0] === "--check" ? "Generated Agent Skill is current." : "Generated Agent Skill.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
await main();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { DECISION_DEFAULT_POLICY } from "../src/core/decision/constants.js";
|
|
3
|
+
import { createTypesafeEngine } from "../src/adapters/typesafe/engine.js";
|
|
4
|
+
|
|
5
|
+
const engine = createTypesafeEngine({ policy: DECISION_DEFAULT_POLICY });
|
|
6
|
+
const result = await engine.health();
|
|
7
|
+
const status = result.status === "missing" ? "NOT_RUN" : result.status;
|
|
8
|
+
console.log(JSON.stringify({
|
|
9
|
+
status,
|
|
10
|
+
provider: result.engine,
|
|
11
|
+
model: result.model,
|
|
12
|
+
...(result.latencyMs !== undefined ? { latencyMs: result.latencyMs } : {}),
|
|
13
|
+
...(result.usage ? { usage: result.usage } : {}),
|
|
14
|
+
...(result.errorCode ? { errorCode: result.errorCode } : {}),
|
|
15
|
+
...(result.providerErrorType ? { providerErrorType: result.providerErrorType } : {}),
|
|
16
|
+
...(result.httpStatus ? { httpStatus: result.httpStatus } : {}),
|
|
17
|
+
...(result.requestId ? { requestId: result.requestId } : {}),
|
|
18
|
+
}, null, 2));
|
|
19
|
+
if (status !== "healthy" && status !== "NOT_RUN") process.exitCode = 1;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- Generated by scripts/generate-forgeloop-skill.mjs. Do not edit manually. -->
|
|
2
|
+
|
|
3
|
+
# ForgeLoop Agent Skill
|
|
4
|
+
|
|
5
|
+
This directory contains a generated, portable, harness-neutral Agent Skill. It is operational guidance, not protocol authority, and has no dependency on external Agent Skills tooling.
|
|
6
|
+
|
|
7
|
+
Canonical hierarchy: runtime registries and validators -> canonical protocol docs -> `docs/AGENT_PROTOCOL_SUMMARY.md` -> this generated projection.
|
|
8
|
+
|
|
9
|
+
Regenerate with `npm run skill:generate`; check exact freshness with `npm run skill:check`. The generator is deterministic, offline, and writes only the five known files in this directory.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: forgeloop
|
|
3
|
+
description: >-
|
|
4
|
+
Operate a repository governed by the ForgeLoop protocol: discover or resume
|
|
5
|
+
tasks, follow canonical next actions, preserve claims and continuity, record
|
|
6
|
+
verification evidence, recover supported protocol state, and require
|
|
7
|
+
validator-backed completion.
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
<!-- Generated by scripts/generate-forgeloop-skill.mjs. Do not edit manually. -->
|
|
11
|
+
|
|
12
|
+
# ForgeLoop
|
|
13
|
+
|
|
14
|
+
## Purpose
|
|
15
|
+
|
|
16
|
+
This skill is operational guidance generated from ForgeLoop's canonical protocol metadata. It is not protocol authority. Always follow the current repository runtime, `protocol-info`, `next`, schemas, validators, and canonical documentation when they differ from this skill.
|
|
17
|
+
|
|
18
|
+
## When to Use
|
|
19
|
+
|
|
20
|
+
Use when ForgeLoop is present in the repository or explicitly requested. Do not activate it for unrelated repositories merely because an agent supports skills.
|
|
21
|
+
|
|
22
|
+
## Source of Truth
|
|
23
|
+
|
|
24
|
+
Runtime registries and validators outrank canonical protocol docs; canonical docs outrank `docs/AGENT_PROTOCOL_SUMMARY.md`; this generated skill is the final non-normative projection. Protocol version is 1. Mutable facts such as phases (RECEIVED, DISCOVERING, CONTRACT_READY, ROUTED, DESIGNING, PLANNED, EXECUTING, VERIFYING, DIAGNOSING, CORRECTING, REVIEWING, COMPLETE, BLOCKED) must be read from the runtime.
|
|
25
|
+
|
|
26
|
+
## Command Resolution
|
|
27
|
+
|
|
28
|
+
In ForgeLoop's own source repository, use `node src/cli.js`. In a consumer repository, use that repository's configured local invocation. Never silently install or upgrade ForgeLoop, and never use a stale global CLI when repository-local code exists. If compatibility is uncertain, inspect `protocol-info --json`.
|
|
29
|
+
|
|
30
|
+
## Operating Workflow
|
|
31
|
+
|
|
32
|
+
1. Discover existing work with `node src/cli.js task-list --json`.
|
|
33
|
+
2. Select or resume the relevant task; a harness, session, model, or context change does not create a new task.
|
|
34
|
+
3. Ask ForgeLoop for `next --task <id> --json` at every boundary and execute only the returned supported action.
|
|
35
|
+
4. Require `preflight` to return `READY` before implementation.
|
|
36
|
+
5. Preserve fresh, validator-owned evidence through verification, review, completion, and the terminal `next` check.
|
|
37
|
+
|
|
38
|
+
## Task Discovery and Continuation
|
|
39
|
+
|
|
40
|
+
Respect active write claims. Do not steal, override, manually release, or edit ownership artifacts. Reconcile continuity with canonical state rather than trusting stale narrative.
|
|
41
|
+
|
|
42
|
+
## Verification Rules
|
|
43
|
+
|
|
44
|
+
`run-check` executes the exact command and records ForgeLoop-owned provenance. `record-check` is serialization-only; its command text is metadata and is never executed. Claims, provider/advisory output, screenshots, and actor summaries are not evidence by themselves. Missing tools remain unavailable; do not auto-install a verifier.
|
|
45
|
+
|
|
46
|
+
## Recovery Rules
|
|
47
|
+
|
|
48
|
+
Use supported recovery only. Preserve failed evidence and follow `next` through diagnosis/correction. For state or ledger divergence, stop lifecycle mutation and inspect validation/classification. Never edit task state, receipts, recovery, continuity, claims, hashes, or `events.ndjson` manually. Reconcile `COMMIT_UNKNOWN` before any retry.
|
|
49
|
+
|
|
50
|
+
## Completion Rule
|
|
51
|
+
|
|
52
|
+
Never claim ForgeLoop-verified completion unless `complete` returns `VALID`. Green tests, CI, a merged PR, or provider output alone are insufficient. After `VALID`, run `next --task <id> --json` again and require the canonical terminal action.
|
|
53
|
+
|
|
54
|
+
## Cross-Harness Continuity
|
|
55
|
+
|
|
56
|
+
A harness or session change does not create a new task. Use supported continuity or handoff commands and reconcile persisted state before continuing.
|
|
57
|
+
|
|
58
|
+
## Durable Action Safety
|
|
59
|
+
|
|
60
|
+
For external actions use propose, authorize, execute, then verify or reconcile. Never blindly retry an uncertain external commit.
|
|
61
|
+
|
|
62
|
+
## Never Do
|
|
63
|
+
|
|
64
|
+
- Never manually assign COMPLETE.
|
|
65
|
+
- Never invent lifecycle chronology or override `next`.
|
|
66
|
+
- Never override active claims or rewrite the append-only ledger.
|
|
67
|
+
- Never fabricate evidence or treat `record-check` as execution.
|
|
68
|
+
- Never auto-install ForgeLoop or a verifier during normal operation.
|
|
69
|
+
- Never duplicate a task because the harness changed.
|
|
70
|
+
- Never treat provider/advisory output as protocol authority.
|
|
71
|
+
|
|
72
|
+
## References
|
|
73
|
+
|
|
74
|
+
- [Canonical Agent Protocol Summary](../../docs/AGENT_PROTOCOL_SUMMARY.md)
|
|
75
|
+
- [Lifecycle](./references/lifecycle.md)
|
|
76
|
+
- [Verification](./references/verification.md)
|
|
77
|
+
- [Recovery](./references/recovery.md)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<!-- Generated by scripts/generate-forgeloop-skill.mjs. Do not edit manually. -->
|
|
2
|
+
|
|
3
|
+
# Lifecycle
|
|
4
|
+
|
|
5
|
+
The canonical loop is task discovery, contract, route, preflight, execution, verification, review, validator-backed completion, and a terminal next check. Always query `next` rather than inferring a transition. The initial post-task-create shape may have no work state: `next` returns DISCOVER, then CREATE_CONTRACT, and only a real contract fingerprint permits the first work-state checkpoint.
|
|
6
|
+
|
|
7
|
+
Phases are runtime-owned and currently include: RECEIVED, DISCOVERING, CONTRACT_READY, ROUTED, DESIGNING, PLANNED, EXECUTING, VERIFYING, DIAGNOSING, CORRECTING, REVIEWING, COMPLETE, BLOCKED.
|
|
8
|
+
|
|
9
|
+
<!-- Canonical summary fingerprint source: docs/AGENT_PROTOCOL_SUMMARY.md -->
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!-- Generated by scripts/generate-forgeloop-skill.mjs. Do not edit manually. -->
|
|
2
|
+
|
|
3
|
+
# Recovery
|
|
4
|
+
|
|
5
|
+
Fail closed on invalid ownership, state, ledger, receipt, or continuity. Use supported recovery commands only. Never edit .forgeloop/task-state, work-state.json, events.ndjson, recovery.json, claims, or hashes by hand. Claim release is not a lifecycle bypass. Reconcile COMMIT_UNKNOWN before retrying any external action.
|
|
6
|
+
|
|
7
|
+
<!-- Canonical summary fingerprint source: docs/AGENT_PROTOCOL_SUMMARY.md -->
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<!-- Generated by scripts/generate-forgeloop-skill.mjs. Do not edit manually. -->
|
|
2
|
+
|
|
3
|
+
# Verification
|
|
4
|
+
|
|
5
|
+
Use run-check for exact command execution and provenance. Use record-check only for supplied observations; it never launches command text. Evidence must be fresh, task-bound, requirement-bound, and validator-readable. Preserve failures, report unavailable tools, and require observed coverage before review and complete -> VALID.
|
|
6
|
+
|
|
7
|
+
<!-- Canonical summary fingerprint source: docs/AGENT_PROTOCOL_SUMMARY.md -->
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { E_BROWSER_VERIFICATION_RESULT_INVALID } from "../../core/error-codes.js";
|
|
2
|
+
|
|
3
|
+
export function actualValue(data) {
|
|
4
|
+
if (typeof data === "string" || typeof data === "number" || typeof data === "boolean") return String(data);
|
|
5
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return "";
|
|
6
|
+
for (const key of ["text", "value", "attribute", "url", "title", "visible"]) {
|
|
7
|
+
if (data[key] !== undefined && data[key] !== null) return String(data[key]);
|
|
8
|
+
}
|
|
9
|
+
return "";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function observedScalar(data, label = "Agent Browser observation") {
|
|
13
|
+
if (typeof data === "string" || typeof data === "number" || typeof data === "boolean") {
|
|
14
|
+
return String(data);
|
|
15
|
+
}
|
|
16
|
+
if (data && typeof data === "object" && !Array.isArray(data)) {
|
|
17
|
+
for (const key of ["text", "value", "attribute", "url", "title", "visible"]) {
|
|
18
|
+
if (Object.prototype.hasOwnProperty.call(data, key) && data[key] !== undefined && data[key] !== null) {
|
|
19
|
+
const value = data[key];
|
|
20
|
+
if (["string", "number", "boolean"].includes(typeof value)) return String(value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const error = new Error(`${label} was malformed`);
|
|
25
|
+
error.code = E_BROWSER_VERIFICATION_RESULT_INVALID;
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function matchesAssertion(kind, actual, expected) {
|
|
30
|
+
if (kind === "VISIBLE") return actual === "true";
|
|
31
|
+
if (kind === "HIDDEN") return actual !== "true";
|
|
32
|
+
if (kind === "TEXT_CONTAINS") return actual.includes(expected);
|
|
33
|
+
if (kind === "TEXT_EQUALS" || kind === "VALUE_EQUALS" || kind === "ATTRIBUTE_EQUALS" || kind === "TITLE_EQUALS") return actual === expected;
|
|
34
|
+
if (kind === "URL_IS") return actual === expected;
|
|
35
|
+
if (kind === "URL_PREFIX") return actual.startsWith(expected);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function assertionResult(assertion, status, actual, message) {
|
|
40
|
+
return {
|
|
41
|
+
id: assertion.id,
|
|
42
|
+
kind: assertion.kind,
|
|
43
|
+
status,
|
|
44
|
+
...(actual !== undefined ? { actual: String(actual) } : {}),
|
|
45
|
+
...(message ? { message } : {}),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const SAFE_GLOBAL_FLAGS = Object.freeze(["--content-boundaries", "--no-webmcp"]);
|
|
2
|
+
|
|
3
|
+
function globalArgs({ sessionId, allowedOrigins, maxOutputChars = 50_000 } = {}) {
|
|
4
|
+
const domains = [...new Set((allowedOrigins ?? []).map((origin) => new URL(origin).hostname.toLowerCase()))].join(",");
|
|
5
|
+
return [
|
|
6
|
+
"--session", sessionId,
|
|
7
|
+
"--allowed-domains", domains,
|
|
8
|
+
"--max-output", String(maxOutputChars),
|
|
9
|
+
...SAFE_GLOBAL_FLAGS,
|
|
10
|
+
];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function command({ sessionId, allowedOrigins, maxOutputChars, name, args = [], json = true }) {
|
|
14
|
+
return [
|
|
15
|
+
...globalArgs({ sessionId, allowedOrigins, maxOutputChars }),
|
|
16
|
+
name,
|
|
17
|
+
...args,
|
|
18
|
+
...(json ? ["--json"] : []),
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function versionCommand() { return ["--version"]; }
|
|
23
|
+
export function openCommand(input) { return command({ ...input, name: "open", args: [input.url] }); }
|
|
24
|
+
export function clickCommand(input) { return command({ ...input, name: "click", args: [input.selector] }); }
|
|
25
|
+
export function focusCommand(input) { return command({ ...input, name: "focus", args: [input.selector] }); }
|
|
26
|
+
export function fillCommand(input) { return command({ ...input, name: "fill", args: [input.selector, input.text] }); }
|
|
27
|
+
export function pressCommand(input) { return command({ ...input, name: "press", args: [input.key] }); }
|
|
28
|
+
export function snapshotCommand(input) { return command({ ...input, name: "snapshot", args: ["-i"] }); }
|
|
29
|
+
export function visibleCommand(input) { return command({ ...input, name: "is", args: ["visible", input.selector] }); }
|
|
30
|
+
export function textCommand(input) { return command({ ...input, name: "get", args: ["text", input.selector] }); }
|
|
31
|
+
export function valueCommand(input) { return command({ ...input, name: "get", args: ["value", input.selector] }); }
|
|
32
|
+
export function attributeCommand(input) { return command({ ...input, name: "get", args: ["attr", input.selector, input.attribute] }); }
|
|
33
|
+
export function urlCommand(input) { return command({ ...input, name: "get", args: ["url"] }); }
|
|
34
|
+
export function titleCommand(input) { return command({ ...input, name: "get", args: ["title"] }); }
|
|
35
|
+
export function screenshotCommand(input) { return command({ ...input, name: "screenshot", args: [input.outputPath], json: true }); }
|
|
36
|
+
export function closeCommand(input) { return command({ ...input, name: "close", args: [] }); }
|
|
37
|
+
|
|
38
|
+
export const AGENT_BROWSER_COMMANDS = Object.freeze({
|
|
39
|
+
version: versionCommand,
|
|
40
|
+
open: openCommand,
|
|
41
|
+
click: clickCommand,
|
|
42
|
+
focus: focusCommand,
|
|
43
|
+
fill: fillCommand,
|
|
44
|
+
press: pressCommand,
|
|
45
|
+
snapshot: snapshotCommand,
|
|
46
|
+
visible: visibleCommand,
|
|
47
|
+
text: textCommand,
|
|
48
|
+
value: valueCommand,
|
|
49
|
+
attribute: attributeCommand,
|
|
50
|
+
url: urlCommand,
|
|
51
|
+
title: titleCommand,
|
|
52
|
+
screenshot: screenshotCommand,
|
|
53
|
+
close: closeCommand,
|
|
54
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { E_BROWSER_VERIFICATION_RESULT_INVALID } from "../../core/error-codes.js";
|
|
2
|
+
|
|
3
|
+
function locatorError(message) {
|
|
4
|
+
const error = new Error(message);
|
|
5
|
+
error.code = E_BROWSER_VERIFICATION_RESULT_INVALID;
|
|
6
|
+
error.kind = "BLOCKED";
|
|
7
|
+
return error;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function refsFromSnapshot(snapshot) {
|
|
11
|
+
if (!snapshot || typeof snapshot !== "object" || Array.isArray(snapshot)) return [];
|
|
12
|
+
const refs = snapshot.refs;
|
|
13
|
+
if (!refs || typeof refs !== "object" || Array.isArray(refs)) return [];
|
|
14
|
+
return Object.entries(refs).map(([ref, value]) => ({ ref, ...(value && typeof value === "object" ? value : { name: value }) }));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function candidateText(value) {
|
|
18
|
+
return [value?.name, value?.text, value?.label, value?.value].filter((item) => typeof item === "string");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveSnapshotLocator(locator, snapshot) {
|
|
22
|
+
if (!locator || typeof locator !== "object") throw locatorError("Browser locator is invalid");
|
|
23
|
+
if (locator.kind === "CSS") return locator.value;
|
|
24
|
+
const refs = refsFromSnapshot(snapshot);
|
|
25
|
+
const needle = locator.value;
|
|
26
|
+
const matches = refs.filter((entry) => {
|
|
27
|
+
const values = candidateText(entry);
|
|
28
|
+
if (locator.kind === "TEXT" || locator.kind === "LABEL") return values.some((value) => value === needle);
|
|
29
|
+
if (locator.kind === "ROLE") {
|
|
30
|
+
return entry.role === needle || values.some((value) => `${entry.role ?? ""} ${value}`.trim() === needle);
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
});
|
|
34
|
+
if (matches.length !== 1) {
|
|
35
|
+
throw locatorError(matches.length === 0 ? "Browser locator was not found" : "Browser locator was ambiguous");
|
|
36
|
+
}
|
|
37
|
+
return matches[0].ref;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function snapshotRefs(snapshot) { return refsFromSnapshot(snapshot); }
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
E_BROWSER_VERIFICATION_OUTPUT_LIMIT,
|
|
7
|
+
E_BROWSER_VERIFICATION_CANCELLED,
|
|
8
|
+
E_BROWSER_VERIFICATION_EXECUTION_FAILED,
|
|
9
|
+
E_BROWSER_VERIFICATION_PROVIDER_INVALID,
|
|
10
|
+
E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE,
|
|
11
|
+
E_BROWSER_VERIFICATION_RESULT_INVALID,
|
|
12
|
+
E_BROWSER_VERIFICATION_TIMEOUT,
|
|
13
|
+
} from "../../core/error-codes.js";
|
|
14
|
+
|
|
15
|
+
export const AGENT_BROWSER_PROCESS_LIMITS = Object.freeze({
|
|
16
|
+
maxStdoutBytes: 1024 * 1024,
|
|
17
|
+
maxStderrBytes: 64 * 1024,
|
|
18
|
+
terminationGraceMs: 150,
|
|
19
|
+
maxVersionBytes: 64 * 1024,
|
|
20
|
+
maxOutputChars: 50_000,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function processError(code, message) {
|
|
24
|
+
const error = new Error(message);
|
|
25
|
+
error.name = "AgentBrowserProcessError";
|
|
26
|
+
error.code = code;
|
|
27
|
+
return error;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function portableString(value, label) {
|
|
31
|
+
if (typeof value !== "string" || value.length === 0 || /\p{Cc}/u.test(value)) {
|
|
32
|
+
throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, `${label} must be a non-empty portable string`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function assertAbsoluteRegularFile(value, label, fsImpl = fs) {
|
|
38
|
+
portableString(value, label);
|
|
39
|
+
if (!path.isAbsolute(value)) {
|
|
40
|
+
throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, `${label} must be an absolute path`);
|
|
41
|
+
}
|
|
42
|
+
let link;
|
|
43
|
+
let stat;
|
|
44
|
+
try {
|
|
45
|
+
link = fsImpl.lstatSync(value);
|
|
46
|
+
stat = fsImpl.statSync(value);
|
|
47
|
+
} catch {
|
|
48
|
+
throw processError(E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE, `${label} is unavailable`);
|
|
49
|
+
}
|
|
50
|
+
if (link.isSymbolicLink?.() || !stat.isFile?.()) {
|
|
51
|
+
throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, `${label} must be a regular file without symlink indirection`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function bytes(chunk) {
|
|
57
|
+
return Buffer.isBuffer(chunk) ? chunk.byteLength : Buffer.byteLength(String(chunk));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function terminate(child) {
|
|
61
|
+
if (!child || typeof child.kill !== "function") return;
|
|
62
|
+
try { child.kill("SIGTERM"); } catch { /* exited between checks */ }
|
|
63
|
+
const timer = setTimeout(() => {
|
|
64
|
+
try { child.kill("SIGKILL"); } catch { /* preserve original failure */ }
|
|
65
|
+
}, AGENT_BROWSER_PROCESS_LIMITS.terminationGraceMs);
|
|
66
|
+
timer.unref?.();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function waitForClose(child) {
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
let settled = false;
|
|
72
|
+
const finish = () => {
|
|
73
|
+
if (settled) return;
|
|
74
|
+
settled = true;
|
|
75
|
+
clearTimeout(timer);
|
|
76
|
+
resolve();
|
|
77
|
+
};
|
|
78
|
+
child?.once?.("close", finish);
|
|
79
|
+
const timer = setTimeout(finish, AGENT_BROWSER_PROCESS_LIMITS.terminationGraceMs + 500);
|
|
80
|
+
timer.unref?.();
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function filteredEnvironment(overrides = {}, trustedEnvKeys = new Set()) {
|
|
85
|
+
const forbidden = new Set([
|
|
86
|
+
"AGENT_BROWSER_PROVIDER", "AGENT_BROWSER_PROFILE", "AGENT_BROWSER_RESTORE", "AGENT_BROWSER_STATE",
|
|
87
|
+
"AGENT_BROWSER_CDP", "AGENT_BROWSER_AUTO_CONNECT", "AGENT_BROWSER_PLUGINS", "AGENT_BROWSER_INIT_SCRIPTS",
|
|
88
|
+
"AGENT_BROWSER_EXTENSIONS", "AGENT_BROWSER_ALLOWED_DOMAINS", "AGENT_BROWSER_MAX_OUTPUT",
|
|
89
|
+
"AGENT_BROWSER_CONTENT_BOUNDARIES", "AGENT_BROWSER_NO_WEBMCP", "AGENT_BROWSER_EXECUTABLE_PATH",
|
|
90
|
+
"AGENT_BROWSER_SESSION", "AGENT_BROWSER_SCREENSHOT_DIR", "AGENT_BROWSER_DOWNLOAD_PATH",
|
|
91
|
+
"AGENT_BROWSER_ACTION_POLICY", "AGENT_BROWSER_CONFIRM_ACTIONS", "AGENT_BROWSER_ENGINE",
|
|
92
|
+
"AI_GATEWAY_API_KEY", "AI_GATEWAY_MODEL", "AI_GATEWAY_URL", "BROWSERBASE_API_KEY",
|
|
93
|
+
"BROWSERLESS_API_KEY", "BROWSER_USE_API_KEY", "KERNEL_API_KEY", "AGENT_BROWSER_CONFIG",
|
|
94
|
+
]);
|
|
95
|
+
const env = {};
|
|
96
|
+
for (const [key, value] of Object.entries({ ...process.env, ...overrides })) {
|
|
97
|
+
if ((!forbidden.has(key) || trustedEnvKeys.has(key)) && typeof value === "string") env[key] = value;
|
|
98
|
+
}
|
|
99
|
+
return env;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function runAgentBrowserCommand(executablePath, args, {
|
|
103
|
+
cwd,
|
|
104
|
+
timeoutMs,
|
|
105
|
+
signal,
|
|
106
|
+
spawnImpl = nodeSpawn,
|
|
107
|
+
env,
|
|
108
|
+
maxStdoutBytes = AGENT_BROWSER_PROCESS_LIMITS.maxStdoutBytes,
|
|
109
|
+
maxStderrBytes = AGENT_BROWSER_PROCESS_LIMITS.maxStderrBytes,
|
|
110
|
+
fsImpl = fs,
|
|
111
|
+
trustedEnvKeys = [],
|
|
112
|
+
} = {}) {
|
|
113
|
+
assertAbsoluteRegularFile(executablePath, "Agent Browser executable", fsImpl);
|
|
114
|
+
portableString(cwd, "Agent Browser cwd");
|
|
115
|
+
if (!path.isAbsolute(cwd)) throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser cwd must be absolute");
|
|
116
|
+
if (!Array.isArray(args) || args.some((arg) => typeof arg !== "string" || /\p{Cc}/u.test(arg))) {
|
|
117
|
+
throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser argv must be a portable string array");
|
|
118
|
+
}
|
|
119
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
120
|
+
throw processError(E_BROWSER_VERIFICATION_PROVIDER_INVALID, "Agent Browser timeout must be a positive integer");
|
|
121
|
+
}
|
|
122
|
+
if (signal?.aborted) throw processError(E_BROWSER_VERIFICATION_CANCELLED, "Agent Browser verification was cancelled");
|
|
123
|
+
|
|
124
|
+
let child;
|
|
125
|
+
try {
|
|
126
|
+
child = spawnImpl(executablePath, [...args], {
|
|
127
|
+
cwd,
|
|
128
|
+
shell: false,
|
|
129
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
130
|
+
env: filteredEnvironment(env, new Set(trustedEnvKeys)),
|
|
131
|
+
});
|
|
132
|
+
} catch (error) {
|
|
133
|
+
throw processError(
|
|
134
|
+
error?.code === "ENOENT" ? E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE : E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE,
|
|
135
|
+
"Agent Browser executable could not be started",
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
if (!child?.stdout || !child?.stderr || typeof child.on !== "function") {
|
|
139
|
+
terminate(child);
|
|
140
|
+
throw processError(E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE, "Agent Browser process did not expose safe output streams");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
let stdoutBytes = 0;
|
|
145
|
+
let stderrBytes = 0;
|
|
146
|
+
const stdout = [];
|
|
147
|
+
let settled = false;
|
|
148
|
+
const timerRef = { value: null };
|
|
149
|
+
const cleanup = () => {
|
|
150
|
+
if (timerRef.value) clearTimeout(timerRef.value);
|
|
151
|
+
signal?.removeEventListener?.("abort", abortListener);
|
|
152
|
+
child.stdout.removeAllListeners?.("data");
|
|
153
|
+
child.stderr.removeAllListeners?.("data");
|
|
154
|
+
};
|
|
155
|
+
const fail = (error) => {
|
|
156
|
+
if (settled) return;
|
|
157
|
+
settled = true;
|
|
158
|
+
cleanup();
|
|
159
|
+
terminate(child);
|
|
160
|
+
child.stdout.resume?.();
|
|
161
|
+
child.stderr.resume?.();
|
|
162
|
+
waitForClose(child).then(() => reject(error));
|
|
163
|
+
};
|
|
164
|
+
const finish = (code, signalName) => {
|
|
165
|
+
if (settled) return;
|
|
166
|
+
settled = true;
|
|
167
|
+
cleanup();
|
|
168
|
+
if (code !== 0) {
|
|
169
|
+
reject(processError(E_BROWSER_VERIFICATION_EXECUTION_FAILED, `Agent Browser command failed (${code ?? "null"}/${signalName ?? "none"})`));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
resolve({
|
|
173
|
+
stdout: Buffer.concat(stdout).toString("utf8"),
|
|
174
|
+
stdoutBytes,
|
|
175
|
+
stderrBytes,
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
const abortListener = () => fail(processError(E_BROWSER_VERIFICATION_CANCELLED, "Agent Browser verification was cancelled"));
|
|
179
|
+
child.stdout.on("data", (chunk) => {
|
|
180
|
+
stdoutBytes += bytes(chunk);
|
|
181
|
+
if (stdoutBytes > maxStdoutBytes) {
|
|
182
|
+
fail(processError(E_BROWSER_VERIFICATION_OUTPUT_LIMIT, "Agent Browser stdout exceeded its bound"));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
stdout.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
186
|
+
});
|
|
187
|
+
child.stderr.on("data", (chunk) => {
|
|
188
|
+
stderrBytes += bytes(chunk);
|
|
189
|
+
if (stderrBytes > maxStderrBytes) fail(processError(E_BROWSER_VERIFICATION_OUTPUT_LIMIT, "Agent Browser stderr exceeded its bound"));
|
|
190
|
+
});
|
|
191
|
+
child.once?.("error", () => fail(processError(E_BROWSER_VERIFICATION_PROVIDER_UNAVAILABLE, "Agent Browser process failed")));
|
|
192
|
+
child.once?.("close", finish);
|
|
193
|
+
signal?.addEventListener?.("abort", abortListener, { once: true });
|
|
194
|
+
timerRef.value = setTimeout(() => fail(processError(E_BROWSER_VERIFICATION_TIMEOUT, "Agent Browser command timed out")), timeoutMs);
|
|
195
|
+
timerRef.value.unref?.();
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function parseAgentBrowserJson(stdout, label = "Agent Browser response") {
|
|
200
|
+
if (typeof stdout !== "string" || stdout.length > AGENT_BROWSER_PROCESS_LIMITS.maxStdoutBytes) {
|
|
201
|
+
throw processError(E_BROWSER_VERIFICATION_OUTPUT_LIMIT, `${label} exceeded its bound`);
|
|
202
|
+
}
|
|
203
|
+
let value;
|
|
204
|
+
try { value = JSON.parse(stdout); } catch { throw processError(E_BROWSER_VERIFICATION_RESULT_INVALID, `${label} was not valid JSON`); }
|
|
205
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
206
|
+
throw processError(E_BROWSER_VERIFICATION_RESULT_INVALID, `${label} must be a JSON object`);
|
|
207
|
+
}
|
|
208
|
+
if (value.success !== true || !Object.prototype.hasOwnProperty.call(value, "data")
|
|
209
|
+
|| Object.keys(value).some((key) => !["success", "data"].includes(key))) {
|
|
210
|
+
throw processError(E_BROWSER_VERIFICATION_RESULT_INVALID, `${label} had an unsupported response envelope`);
|
|
211
|
+
}
|
|
212
|
+
return value.data === undefined ? value : value.data;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export { filteredEnvironment, processError as agentBrowserProcessError };
|