xcoder-dna 0.0.0-stage → 1.3.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 +66 -0
- package/README.md +87 -2
- package/dist/agents/ai-chat-debate.d.ts +36 -0
- package/dist/agents/ai-chat-debate.js +300 -0
- package/dist/agents/ai-chat-report-writer.d.ts +7 -0
- package/dist/agents/ai-chat-report-writer.js +100 -0
- package/dist/agents/duplicate-detector.d.ts +41 -0
- package/dist/agents/duplicate-detector.js +65 -0
- package/dist/agents/genesis-agent.d.ts +61 -0
- package/dist/agents/genesis-agent.js +340 -0
- package/dist/agents/samurai-agent.d.ts +53 -0
- package/dist/agents/samurai-agent.js +279 -0
- package/dist/agents/sub-agent.d.ts +56 -0
- package/dist/agents/sub-agent.js +353 -0
- package/dist/backlog/backlog-requirement-validator.d.ts +10 -0
- package/dist/backlog/backlog-requirement-validator.js +60 -0
- package/dist/backlog/backlog-store.d.ts +29 -0
- package/dist/backlog/backlog-store.js +148 -0
- package/dist/backlog/po-agent.d.ts +16 -0
- package/dist/backlog/po-agent.js +121 -0
- package/dist/backlog/types.d.ts +25 -0
- package/dist/backlog/types.js +4 -0
- package/dist/bench/run.d.ts +11 -0
- package/dist/bench/run.js +185 -0
- package/dist/bench/types.d.ts +50 -0
- package/dist/bench/types.js +1 -0
- package/dist/cli/swarm.d.ts +10 -0
- package/dist/cli/swarm.js +100 -0
- package/dist/cli/task.d.ts +14 -0
- package/dist/cli/task.js +121 -0
- package/dist/cli/wbc-arg.d.ts +6 -0
- package/dist/cli/wbc-arg.js +40 -0
- package/dist/cli/wbc.d.ts +45 -0
- package/dist/cli/wbc.js +98 -0
- package/dist/commands/license.d.ts +9 -0
- package/dist/commands/license.js +140 -0
- package/dist/commands/release.d.ts +44 -0
- package/dist/commands/release.js +180 -0
- package/dist/commands/test.d.ts +50 -0
- package/dist/commands/test.js +385 -0
- package/dist/config/loadConfig.d.ts +82 -0
- package/dist/config/loadConfig.js +240 -0
- package/dist/config/secure-mode.d.ts +29 -0
- package/dist/config/secure-mode.js +63 -0
- package/dist/config/user-config.d.ts +39 -0
- package/dist/config/user-config.js +202 -0
- package/dist/engine/reasoning-engine.d.ts +21 -0
- package/dist/engine/reasoning-engine.js +59 -0
- package/dist/events/bus.d.ts +22 -0
- package/dist/events/bus.js +18 -0
- package/dist/index.d.ts +79 -0
- package/dist/index.js +77 -0
- package/dist/indexing/ignoreRules.d.ts +1 -0
- package/dist/indexing/ignoreRules.js +53 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.d.ts +57 -0
- package/dist/instance-swarm/instance-swarm-orchestrator.js +204 -0
- package/dist/instance-swarm/protocol-name-resolver.d.ts +23 -0
- package/dist/instance-swarm/protocol-name-resolver.js +64 -0
- package/dist/license/gate.d.ts +31 -0
- package/dist/license/gate.js +188 -0
- package/dist/license/http-client.d.ts +43 -0
- package/dist/license/http-client.js +195 -0
- package/dist/license/manager.d.ts +39 -0
- package/dist/license/manager.js +175 -0
- package/dist/license/receipt.d.ts +27 -0
- package/dist/license/receipt.js +108 -0
- package/dist/license/state-codec.d.ts +72 -0
- package/dist/license/state-codec.js +218 -0
- package/dist/license/state-paths.d.ts +17 -0
- package/dist/license/state-paths.js +64 -0
- package/dist/license/store.d.ts +44 -0
- package/dist/license/store.js +223 -0
- package/dist/license/trust-anchor.d.ts +24 -0
- package/dist/license/trust-anchor.js +74 -0
- package/dist/license/types.d.ts +34 -0
- package/dist/license/types.js +19 -0
- package/dist/llm/budget.d.ts +23 -0
- package/dist/llm/budget.js +58 -0
- package/dist/llm/client.d.ts +53 -0
- package/dist/llm/client.js +122 -0
- package/dist/llm/factory.d.ts +14 -0
- package/dist/llm/factory.js +32 -0
- package/dist/orchestrator/defect-logger.d.ts +31 -0
- package/dist/orchestrator/defect-logger.js +101 -0
- package/dist/orchestrator/defect-manager.d.ts +118 -0
- package/dist/orchestrator/defect-manager.js +338 -0
- package/dist/orchestrator/defect-migration.d.ts +14 -0
- package/dist/orchestrator/defect-migration.js +174 -0
- package/dist/orchestrator/defect-store.d.ts +48 -0
- package/dist/orchestrator/defect-store.js +76 -0
- package/dist/orchestrator/healing.d.ts +14 -0
- package/dist/orchestrator/healing.js +11 -0
- package/dist/orchestrator/interactive-steering.d.ts +64 -0
- package/dist/orchestrator/interactive-steering.js +107 -0
- package/dist/orchestrator/interrupt-handler.d.ts +30 -0
- package/dist/orchestrator/interrupt-handler.js +70 -0
- package/dist/orchestrator/lesson-manager.d.ts +25 -0
- package/dist/orchestrator/lesson-manager.js +122 -0
- package/dist/orchestrator/progress-summary.d.ts +15 -0
- package/dist/orchestrator/progress-summary.js +106 -0
- package/dist/orchestrator/reasoning-score.d.ts +49 -0
- package/dist/orchestrator/reasoning-score.js +117 -0
- package/dist/orchestrator/swarm.d.ts +36 -0
- package/dist/orchestrator/swarm.js +77 -0
- package/dist/orchestrator/task-factory.d.ts +15 -0
- package/dist/orchestrator/task-factory.js +176 -0
- package/dist/orchestrator/validation-gate.d.ts +23 -0
- package/dist/orchestrator/validation-gate.js +155 -0
- package/dist/protocol/react-contract.d.ts +136 -0
- package/dist/protocol/react-contract.js +698 -0
- package/dist/protocol/wks-protocol-manager.d.ts +22 -0
- package/dist/protocol/wks-protocol-manager.js +72 -0
- package/dist/sdlc-runtime/conversation-runner.d.ts +8 -0
- package/dist/sdlc-runtime/conversation-runner.js +64 -0
- package/dist/sdlc-runtime/dag-utils.d.ts +15 -0
- package/dist/sdlc-runtime/dag-utils.js +67 -0
- package/dist/sdlc-runtime/defect-integration.d.ts +7 -0
- package/dist/sdlc-runtime/defect-integration.js +99 -0
- package/dist/sdlc-runtime/execution-loop.d.ts +38 -0
- package/dist/sdlc-runtime/execution-loop.js +274 -0
- package/dist/sdlc-runtime/lesson-recorder.d.ts +9 -0
- package/dist/sdlc-runtime/lesson-recorder.js +27 -0
- package/dist/sdlc-runtime/plan-initializer.d.ts +47 -0
- package/dist/sdlc-runtime/plan-initializer.js +135 -0
- package/dist/sdlc-runtime/progress-persister.d.ts +10 -0
- package/dist/sdlc-runtime/progress-persister.js +34 -0
- package/dist/sdlc-runtime/protocol-loader.d.ts +10 -0
- package/dist/sdlc-runtime/protocol-loader.js +21 -0
- package/dist/sdlc-runtime/reporting.d.ts +24 -0
- package/dist/sdlc-runtime/reporting.js +68 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.d.ts +59 -0
- package/dist/sdlc-runtime/sdlc-orchestrator.js +170 -0
- package/dist/sdlc-runtime/sdlc-skill.d.ts +44 -0
- package/dist/sdlc-runtime/sdlc-skill.js +148 -0
- package/dist/sdlc-runtime/steering-handler.d.ts +14 -0
- package/dist/sdlc-runtime/steering-handler.js +51 -0
- package/dist/sdlc-runtime/trace-recorder.d.ts +10 -0
- package/dist/sdlc-runtime/trace-recorder.js +7 -0
- package/dist/security/index.d.ts +16 -0
- package/dist/security/index.js +12 -0
- package/dist/security/input-screen.d.ts +30 -0
- package/dist/security/input-screen.js +75 -0
- package/dist/security/tool-call-guard.d.ts +16 -0
- package/dist/security/tool-call-guard.js +163 -0
- package/dist/security/untrusted-content.d.ts +34 -0
- package/dist/security/untrusted-content.js +63 -0
- package/dist/skills/advanced-dfir/SKILL.md +85 -0
- package/dist/skills/advanced-pentest-lab/SKILL.md +88 -0
- package/dist/skills/analyst/SKILL.md +55 -0
- package/dist/skills/architect/SKILL.md +54 -0
- package/dist/skills/aws/SKILL.md +92 -0
- package/dist/skills/azure/SKILL.md +90 -0
- package/dist/skills/bylaw-context.d.ts +45 -0
- package/dist/skills/bylaw-context.js +61 -0
- package/dist/skills/bylaw-review.d.ts +20 -0
- package/dist/skills/bylaw-review.js +101 -0
- package/dist/skills/conversation/SKILL.md +29 -0
- package/dist/skills/design-patterns/SKILL.md +190 -0
- package/dist/skills/devops/SKILL.md +55 -0
- package/dist/skills/directives.d.ts +20 -0
- package/dist/skills/directives.js +313 -0
- package/dist/skills/docker/SKILL.md +76 -0
- package/dist/skills/docker-expert/SKILL.md +56 -0
- package/dist/skills/enterprise-systems-analyst/SKILL.md +83 -0
- package/dist/skills/enterprise-systems-analyst/references/crm.md +41 -0
- package/dist/skills/enterprise-systems-analyst/references/erp.md +40 -0
- package/dist/skills/enterprise-systems-analyst/references/financial-systems.md +35 -0
- package/dist/skills/enterprise-systems-analyst/references/hcm.md +38 -0
- package/dist/skills/enterprise-systems-analyst/references/mrp.md +37 -0
- package/dist/skills/enterprise-systems-analyst/references/scm.md +32 -0
- package/dist/skills/ethical-hacking-methodology/SKILL.md +119 -0
- package/dist/skills/externalSkillLoader.d.ts +25 -0
- package/dist/skills/externalSkillLoader.js +65 -0
- package/dist/skills/externalSkillRegistry.d.ts +33 -0
- package/dist/skills/externalSkillRegistry.js +94 -0
- package/dist/skills/filesystem-management/SKILL.md +61 -0
- package/dist/skills/first-run-seed.d.ts +26 -0
- package/dist/skills/first-run-seed.js +75 -0
- package/dist/skills/git-vcs/SKILL.md +84 -0
- package/dist/skills/kafka/SKILL.md +89 -0
- package/dist/skills/kubernetes/SKILL.md +93 -0
- package/dist/skills/kubernetes-expert/SKILL.md +54 -0
- package/dist/skills/openshift/SKILL.md +93 -0
- package/dist/skills/pentester/SKILL.md +59 -0
- package/dist/skills/performance-tester/SKILL.md +57 -0
- package/dist/skills/playwright-ui-tester/SKILL.md +67 -0
- package/dist/skills/programmer/SKILL.md +58 -0
- package/dist/skills/project-scaffolding/SKILL.md +133 -0
- package/dist/skills/qa-engineer/SKILL.md +91 -0
- package/dist/skills/rca/SKILL.md +58 -0
- package/dist/skills/redhat/SKILL.md +92 -0
- package/dist/skills/rosa/SKILL.md +80 -0
- package/dist/skills/scrum-framework/SKILL.md +74 -0
- package/dist/skills/scrum-framework/references/artifacts_and_quality.md +79 -0
- package/dist/skills/scrum-framework/references/roles_and_events.md +105 -0
- package/dist/skills/scrum-master-agent/SKILL.md +74 -0
- package/dist/skills/scrum-master-agent/references/behaviors.md +56 -0
- package/dist/skills/sdlc-skill.d.ts +1 -0
- package/dist/skills/sdlc-skill.js +4 -0
- package/dist/skills/secops/SKILL.md +52 -0
- package/dist/skills/secure-architect.bylaws.d.ts +68 -0
- package/dist/skills/secure-architect.bylaws.js +525 -0
- package/dist/skills/seed-skills.d.ts +48 -0
- package/dist/skills/seed-skills.js +213 -0
- package/dist/skills/skill-authoring/SKILL.md +123 -0
- package/dist/skills/skill-header.d.ts +51 -0
- package/dist/skills/skill-header.js +163 -0
- package/dist/skills/skill-path-guard.d.ts +41 -0
- package/dist/skills/skill-path-guard.js +95 -0
- package/dist/skills/skillRouter.d.ts +51 -0
- package/dist/skills/skillRouter.js +90 -0
- package/dist/skills/skills-root.d.ts +64 -0
- package/dist/skills/skills-root.js +168 -0
- package/dist/skills/software-architect/SKILL.md +77 -0
- package/dist/skills/software-engineer/SKILL.md +74 -0
- package/dist/skills/solid-principles/SKILL.md +94 -0
- package/dist/skills/task-planning/SKILL.md +115 -0
- package/dist/skills/tester/SKILL.md +54 -0
- package/dist/skills/ubuntu/SKILL.md +87 -0
- package/dist/skills/ui-ux-design/SKILL.md +114 -0
- package/dist/skills/workspace-context/SKILL.md +103 -0
- package/dist/sprint/controller.d.ts +37 -0
- package/dist/sprint/controller.js +220 -0
- package/dist/sprint/dag-builder.d.ts +14 -0
- package/dist/sprint/dag-builder.js +105 -0
- package/dist/sprint/reporter.d.ts +11 -0
- package/dist/sprint/reporter.js +97 -0
- package/dist/sprint/state-store.d.ts +10 -0
- package/dist/sprint/state-store.js +71 -0
- package/dist/sprint/types.d.ts +24 -0
- package/dist/sprint/types.js +4 -0
- package/dist/store/file-version-store.d.ts +24 -0
- package/dist/store/file-version-store.js +73 -0
- package/dist/store/task-store.d.ts +90 -0
- package/dist/store/task-store.js +166 -0
- package/dist/task-ingest/architect-detailer.d.ts +43 -0
- package/dist/task-ingest/architect-detailer.js +141 -0
- package/dist/task-ingest/detail-schema.d.ts +9 -0
- package/dist/task-ingest/detail-schema.js +195 -0
- package/dist/task-ingest/detailed-run.d.ts +36 -0
- package/dist/task-ingest/detailed-run.js +108 -0
- package/dist/task-ingest/extract-id.d.ts +9 -0
- package/dist/task-ingest/extract-id.js +24 -0
- package/dist/task-ingest/index.d.ts +23 -0
- package/dist/task-ingest/index.js +14 -0
- package/dist/task-ingest/ingest.d.ts +45 -0
- package/dist/task-ingest/ingest.js +163 -0
- package/dist/task-ingest/list-ingested.d.ts +13 -0
- package/dist/task-ingest/list-ingested.js +30 -0
- package/dist/task-ingest/pending-filter.d.ts +30 -0
- package/dist/task-ingest/pending-filter.js +59 -0
- package/dist/task-ingest/scanner.d.ts +56 -0
- package/dist/task-ingest/scanner.js +216 -0
- package/dist/task-ingest/source-reader.d.ts +25 -0
- package/dist/task-ingest/source-reader.js +93 -0
- package/dist/task-ingest/spec-builder.d.ts +34 -0
- package/dist/task-ingest/spec-builder.js +89 -0
- package/dist/task-ingest/wbs-extract.d.ts +50 -0
- package/dist/task-ingest/wbs-extract.js +226 -0
- package/dist/task-ingest/wbs-parser.d.ts +36 -0
- package/dist/task-ingest/wbs-parser.js +170 -0
- package/dist/tools/apiTestTool.d.ts +37 -0
- package/dist/tools/apiTestTool.js +128 -0
- package/dist/tools/ast-tools.d.ts +183 -0
- package/dist/tools/ast-tools.js +853 -0
- package/dist/tools/browse-tool.d.ts +105 -0
- package/dist/tools/browse-tool.js +394 -0
- package/dist/tools/credential.d.ts +17 -0
- package/dist/tools/credential.js +32 -0
- package/dist/tools/dependency-graph.d.ts +23 -0
- package/dist/tools/dependency-graph.js +164 -0
- package/dist/tools/edit-tool.d.ts +34 -0
- package/dist/tools/edit-tool.js +95 -0
- package/dist/tools/file-header-manager.d.ts +27 -0
- package/dist/tools/file-header-manager.js +153 -0
- package/dist/tools/fs-tools.d.ts +154 -0
- package/dist/tools/fs-tools.js +337 -0
- package/dist/tools/git-tools.d.ts +79 -0
- package/dist/tools/git-tools.js +348 -0
- package/dist/tools/github-tool.d.ts +48 -0
- package/dist/tools/github-tool.js +147 -0
- package/dist/tools/globTool.d.ts +20 -0
- package/dist/tools/globTool.js +34 -0
- package/dist/tools/grepTool.d.ts +19 -0
- package/dist/tools/grepTool.js +44 -0
- package/dist/tools/index-freshness.d.ts +58 -0
- package/dist/tools/index-freshness.js +190 -0
- package/dist/tools/index-glob.d.ts +48 -0
- package/dist/tools/index-glob.js +175 -0
- package/dist/tools/load-skill-tool.d.ts +15 -0
- package/dist/tools/load-skill-tool.js +114 -0
- package/dist/tools/patch-tools.d.ts +58 -0
- package/dist/tools/patch-tools.js +153 -0
- package/dist/tools/playwright-tool.d.ts +66 -0
- package/dist/tools/playwright-tool.js +121 -0
- package/dist/tools/project-detect.d.ts +20 -0
- package/dist/tools/project-detect.js +87 -0
- package/dist/tools/prompt-guidance.d.ts +22 -0
- package/dist/tools/prompt-guidance.js +23 -0
- package/dist/tools/scheduleTool.d.ts +19 -0
- package/dist/tools/scheduleTool.js +181 -0
- package/dist/tools/search-tools.d.ts +27 -0
- package/dist/tools/search-tools.js +78 -0
- package/dist/tools/sed-tool.d.ts +66 -0
- package/dist/tools/sed-tool.js +402 -0
- package/dist/tools/shell-result.d.ts +11 -0
- package/dist/tools/shell-result.js +1 -0
- package/dist/tools/shell-tool.d.ts +45 -0
- package/dist/tools/shell-tool.js +141 -0
- package/dist/tools/siteCrawlerTool.d.ts +32 -0
- package/dist/tools/siteCrawlerTool.js +205 -0
- package/dist/tools/tool-registry.d.ts +67 -0
- package/dist/tools/tool-registry.js +596 -0
- package/dist/tools/validate-tool.d.ts +23 -0
- package/dist/tools/validate-tool.js +201 -0
- package/dist/tools/workspace-index.d.ts +46 -0
- package/dist/tools/workspace-index.js +173 -0
- package/dist/types.d.ts +222 -0
- package/dist/types.js +7 -0
- package/dist/updater/index.d.ts +14 -0
- package/dist/updater/index.js +120 -0
- package/dist/util/banner.d.ts +25 -0
- package/dist/util/banner.js +140 -0
- package/dist/util/command-suggester.d.ts +6 -0
- package/dist/util/command-suggester.js +25 -0
- package/dist/util/confirm-interrupt.d.ts +10 -0
- package/dist/util/confirm-interrupt.js +31 -0
- package/dist/util/continue-trigger.d.ts +11 -0
- package/dist/util/continue-trigger.js +20 -0
- package/dist/util/env-info.d.ts +18 -0
- package/dist/util/env-info.js +37 -0
- package/dist/util/file-access-log.d.ts +35 -0
- package/dist/util/file-access-log.js +140 -0
- package/dist/util/footer-manager.d.ts +124 -0
- package/dist/util/footer-manager.js +195 -0
- package/dist/util/footer-renderer.d.ts +67 -0
- package/dist/util/footer-renderer.js +319 -0
- package/dist/util/footer-state.d.ts +34 -0
- package/dist/util/footer-state.js +18 -0
- package/dist/util/footer-text.d.ts +51 -0
- package/dist/util/footer-text.js +74 -0
- package/dist/util/ids.d.ts +1 -0
- package/dist/util/ids.js +4 -0
- package/dist/util/levenshtein.d.ts +6 -0
- package/dist/util/levenshtein.js +34 -0
- package/dist/util/logger.d.ts +10 -0
- package/dist/util/logger.js +41 -0
- package/dist/util/logs.d.ts +26 -0
- package/dist/util/logs.js +88 -0
- package/dist/util/output-handler.d.ts +89 -0
- package/dist/util/output-handler.js +193 -0
- package/dist/util/output-interceptor.d.ts +18 -0
- package/dist/util/output-interceptor.js +49 -0
- package/dist/util/output-paths.d.ts +43 -0
- package/dist/util/output-paths.js +121 -0
- package/dist/util/package-json.d.ts +6 -0
- package/dist/util/package-json.js +36 -0
- package/dist/util/process-kill.d.ts +27 -0
- package/dist/util/process-kill.js +148 -0
- package/dist/util/protocol-error-logger.d.ts +33 -0
- package/dist/util/protocol-error-logger.js +73 -0
- package/dist/util/redact.d.ts +25 -0
- package/dist/util/redact.js +103 -0
- package/dist/util/scroll-region-guard.d.ts +33 -0
- package/dist/util/scroll-region-guard.js +63 -0
- package/dist/util/spinner.d.ts +15 -0
- package/dist/util/spinner.js +49 -0
- package/dist/util/terminal-output.d.ts +34 -0
- package/dist/util/terminal-output.js +1 -0
- package/dist/util/terminal-silent.d.ts +6 -0
- package/dist/util/terminal-silent.js +20 -0
- package/dist/util/terminal.d.ts +56 -0
- package/dist/util/terminal.js +124 -0
- package/dist/util/text-sanitize.d.ts +8 -0
- package/dist/util/text-sanitize.js +45 -0
- package/dist/util/thinking-spinner.d.ts +27 -0
- package/dist/util/thinking-spinner.js +189 -0
- package/dist/util/tool-error-logger.d.ts +33 -0
- package/dist/util/tool-error-logger.js +77 -0
- package/dist/util/tty.d.ts +13 -0
- package/dist/util/tty.js +16 -0
- package/dist/util/xcoder-paths.d.ts +56 -0
- package/dist/util/xcoder-paths.js +63 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +4 -0
- package/dist/worker-agent/to-subtask-node.d.ts +35 -0
- package/dist/worker-agent/to-subtask-node.js +41 -0
- package/dist/worker-agent/worker-orchestrator.d.ts +104 -0
- package/dist/worker-agent/worker-orchestrator.js +201 -0
- package/package.json +39 -4
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `browse` — let the agent (or `xcoder browse …`) drive a real browser through
|
|
3
|
+
* a list of steps: navigate, click, type, read text/links, screenshot, assert.
|
|
4
|
+
* Unlike `ui_test` (pass/fail only) it RETURNS what it read, so the model can
|
|
5
|
+
* decide its next step. Requires the optional `playwright` dependency.
|
|
6
|
+
*
|
|
7
|
+
* Safety:
|
|
8
|
+
* - every request (navigation, redirect, sub-resource) goes through
|
|
9
|
+
* `checkRequestAllowed` — http(s) only, no file:/chrome:/ftp:, and no
|
|
10
|
+
* private-network or cloud-metadata hosts unless XCODER_BROWSE_ALLOW_PRIVATE=1
|
|
11
|
+
* (localhost is always allowed so local apps can be tested);
|
|
12
|
+
* - secrets are typed via `credentialRef` (env var name) and never returned;
|
|
13
|
+
* LLM / GitHub key variables can never be used as a credentialRef;
|
|
14
|
+
* - screenshots can only be written inside the workspace.
|
|
15
|
+
*/
|
|
16
|
+
export type BrowseStep = {
|
|
17
|
+
op: "goto";
|
|
18
|
+
url: string;
|
|
19
|
+
} | {
|
|
20
|
+
op: "click" | "hover" | "check" | "uncheck";
|
|
21
|
+
selector: string;
|
|
22
|
+
} | {
|
|
23
|
+
op: "fill" | "type";
|
|
24
|
+
selector: string;
|
|
25
|
+
value?: string;
|
|
26
|
+
credentialRef?: string;
|
|
27
|
+
} | {
|
|
28
|
+
op: "select";
|
|
29
|
+
selector: string;
|
|
30
|
+
value: string;
|
|
31
|
+
} | {
|
|
32
|
+
op: "press";
|
|
33
|
+
key: string;
|
|
34
|
+
selector?: string;
|
|
35
|
+
} | {
|
|
36
|
+
op: "scroll";
|
|
37
|
+
to: "top" | "bottom" | "up" | "down" | string;
|
|
38
|
+
} | {
|
|
39
|
+
op: "wait_for";
|
|
40
|
+
selector: string;
|
|
41
|
+
state?: "visible" | "hidden" | "attached" | "detached";
|
|
42
|
+
timeoutMs?: number;
|
|
43
|
+
} | {
|
|
44
|
+
op: "wait";
|
|
45
|
+
ms: number;
|
|
46
|
+
} | {
|
|
47
|
+
op: "text";
|
|
48
|
+
selector?: string;
|
|
49
|
+
} | {
|
|
50
|
+
op: "attr";
|
|
51
|
+
selector: string;
|
|
52
|
+
name: string;
|
|
53
|
+
} | {
|
|
54
|
+
op: "links";
|
|
55
|
+
selector?: string;
|
|
56
|
+
} | {
|
|
57
|
+
op: "title" | "url" | "back" | "forward" | "reload" | "close";
|
|
58
|
+
} | {
|
|
59
|
+
op: "screenshot";
|
|
60
|
+
path?: string;
|
|
61
|
+
fullPage?: boolean;
|
|
62
|
+
} | {
|
|
63
|
+
op: "expect_text";
|
|
64
|
+
selector: string;
|
|
65
|
+
text: string;
|
|
66
|
+
} | {
|
|
67
|
+
op: "expect_url";
|
|
68
|
+
contains: string;
|
|
69
|
+
};
|
|
70
|
+
export interface BrowseInput {
|
|
71
|
+
/** Optional starting URL (same as a leading `goto` step). */
|
|
72
|
+
url?: string;
|
|
73
|
+
steps: BrowseStep[];
|
|
74
|
+
/** Keep the browser open between calls under this name (cookies/login survive). End with {op:"close"}. */
|
|
75
|
+
session?: string;
|
|
76
|
+
headed?: boolean;
|
|
77
|
+
/** Per-step timeout, default 10000. */
|
|
78
|
+
timeoutMs?: number;
|
|
79
|
+
}
|
|
80
|
+
export interface BrowseStepResult {
|
|
81
|
+
op: string;
|
|
82
|
+
ok: boolean;
|
|
83
|
+
note: string;
|
|
84
|
+
data?: unknown;
|
|
85
|
+
}
|
|
86
|
+
export interface BrowseResult {
|
|
87
|
+
ok: boolean;
|
|
88
|
+
detail: string;
|
|
89
|
+
steps: BrowseStepResult[];
|
|
90
|
+
page?: {
|
|
91
|
+
url: string;
|
|
92
|
+
title: string;
|
|
93
|
+
text: string;
|
|
94
|
+
};
|
|
95
|
+
screenshots: string[];
|
|
96
|
+
}
|
|
97
|
+
/** Returns a reason string if the URL must be blocked, otherwise null. */
|
|
98
|
+
export declare function checkRequestAllowed(rawUrl: string, opts?: {
|
|
99
|
+
navigation?: boolean;
|
|
100
|
+
}): string | null;
|
|
101
|
+
export declare function closeAllBrowseSessions(): Promise<void>;
|
|
102
|
+
/** Split "SEL=VALUE" at the first `=` that is not inside [] or quotes. */
|
|
103
|
+
export declare function splitSelectorValue(s: string): [string, string | undefined];
|
|
104
|
+
export declare function parseStepSpec(spec: string): BrowseStep;
|
|
105
|
+
export declare function runBrowse(root: string, input: BrowseInput): Promise<BrowseResult>;
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { resolveCredential, redact } from "./credential.js";
|
|
4
|
+
const MAX_TEXT = 4000;
|
|
5
|
+
const MAX_LINKS = 50;
|
|
6
|
+
const MAX_SESSIONS = 3;
|
|
7
|
+
const FORBIDDEN_CREDENTIAL_REFS = /^(XCODER_API_KEY|DEEPSEEK_API_KEY|ANTHROPIC_API_KEY|OPENAI_API_KEY|GITHUB_TOKEN|GH_TOKEN)$/i;
|
|
8
|
+
// ── URL policy ──────────────────────────────────────────────────────────────
|
|
9
|
+
function isPrivateIPv4(host) {
|
|
10
|
+
const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host);
|
|
11
|
+
if (!m)
|
|
12
|
+
return false;
|
|
13
|
+
const [a, b] = [Number(m[1]), Number(m[2])];
|
|
14
|
+
if (a === 127)
|
|
15
|
+
return false; // loopback is allowed (local apps)
|
|
16
|
+
return a === 10 || a === 0 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254) || (a === 100 && b >= 64 && b <= 127);
|
|
17
|
+
}
|
|
18
|
+
function isPrivateIPv6(host) {
|
|
19
|
+
const h = host.replace(/^\[|\]$/g, "").toLowerCase();
|
|
20
|
+
if (!h.includes(":"))
|
|
21
|
+
return false; // a domain name like "fd-cdn.example.com", not an IPv6 literal
|
|
22
|
+
if (h === "::1")
|
|
23
|
+
return false; // loopback allowed
|
|
24
|
+
// IPv4-mapped: WHATWG URL normalises ::ffff:10.0.0.1 to ::ffff:a00:1
|
|
25
|
+
const hex = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(h);
|
|
26
|
+
if (hex) {
|
|
27
|
+
const hi = parseInt(hex[1], 16);
|
|
28
|
+
const lo = parseInt(hex[2], 16);
|
|
29
|
+
return isPrivateIPv4(`${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`);
|
|
30
|
+
}
|
|
31
|
+
if (h.startsWith("::ffff:"))
|
|
32
|
+
return isPrivateIPv4(h.slice(7));
|
|
33
|
+
return /^(fc|fd)/.test(h) || /^fe[89ab]/.test(h) || h === "::";
|
|
34
|
+
}
|
|
35
|
+
/** Returns a reason string if the URL must be blocked, otherwise null. */
|
|
36
|
+
export function checkRequestAllowed(rawUrl, opts = {}) {
|
|
37
|
+
let u;
|
|
38
|
+
try {
|
|
39
|
+
u = new URL(rawUrl);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return `invalid URL "${rawUrl}"`;
|
|
43
|
+
}
|
|
44
|
+
const proto = u.protocol;
|
|
45
|
+
if (proto === "http:" || proto === "https:") {
|
|
46
|
+
const host = u.hostname.toLowerCase();
|
|
47
|
+
if (process.env.XCODER_BROWSE_ALLOW_PRIVATE === "1")
|
|
48
|
+
return null;
|
|
49
|
+
if (host === "metadata.google.internal" || host.endsWith(".internal") || host === "metadata")
|
|
50
|
+
return `host "${host}" is blocked (cloud metadata)`;
|
|
51
|
+
if (isPrivateIPv4(host) || isPrivateIPv6(host)) {
|
|
52
|
+
return `private-network address "${host}" is blocked (set XCODER_BROWSE_ALLOW_PRIVATE=1 to allow; localhost is always allowed)`;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// Sub-resources may be data:/blob:/about:, but navigation must be http(s).
|
|
57
|
+
if (!opts.navigation && (proto === "data:" || proto === "blob:" || proto === "about:"))
|
|
58
|
+
return null;
|
|
59
|
+
return `scheme "${proto}" is not allowed (http and https only)`;
|
|
60
|
+
}
|
|
61
|
+
const sessions = new Map();
|
|
62
|
+
export async function closeAllBrowseSessions() {
|
|
63
|
+
for (const [name, s] of sessions) {
|
|
64
|
+
sessions.delete(name);
|
|
65
|
+
await s.browser.close().catch(() => undefined);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// ── step DSL for the CLI: xcoder browse --step "click:#login" ───────────────
|
|
69
|
+
/** Split "SEL=VALUE" at the first `=` that is not inside [] or quotes. */
|
|
70
|
+
export function splitSelectorValue(s) {
|
|
71
|
+
let depth = 0;
|
|
72
|
+
let quote = "";
|
|
73
|
+
for (let i = 0; i < s.length; i++) {
|
|
74
|
+
const c = s[i];
|
|
75
|
+
if (quote) {
|
|
76
|
+
if (c === quote)
|
|
77
|
+
quote = "";
|
|
78
|
+
}
|
|
79
|
+
else if (c === '"' || c === "'")
|
|
80
|
+
quote = c;
|
|
81
|
+
else if (c === "[")
|
|
82
|
+
depth++;
|
|
83
|
+
else if (c === "]")
|
|
84
|
+
depth = Math.max(0, depth - 1);
|
|
85
|
+
else if (c === "=" && depth === 0)
|
|
86
|
+
return [s.slice(0, i), s.slice(i + 1)];
|
|
87
|
+
}
|
|
88
|
+
return [s, undefined];
|
|
89
|
+
}
|
|
90
|
+
export function parseStepSpec(spec) {
|
|
91
|
+
const raw = spec.trim();
|
|
92
|
+
if (/^https?:\/\//i.test(raw))
|
|
93
|
+
return { op: "goto", url: raw };
|
|
94
|
+
const colon = raw.indexOf(":");
|
|
95
|
+
const op = (colon === -1 ? raw : raw.slice(0, colon)).trim().toLowerCase();
|
|
96
|
+
const rest = colon === -1 ? "" : raw.slice(colon + 1).trim();
|
|
97
|
+
const need = (what) => {
|
|
98
|
+
if (!rest)
|
|
99
|
+
throw new Error(`step "${spec}": ${op} needs ${what}`);
|
|
100
|
+
return rest;
|
|
101
|
+
};
|
|
102
|
+
const pair = (what) => {
|
|
103
|
+
const [sel, val] = splitSelectorValue(need(what));
|
|
104
|
+
if (val === undefined)
|
|
105
|
+
throw new Error(`step "${spec}": expected ${op}:SELECTOR=VALUE`);
|
|
106
|
+
return [sel, val];
|
|
107
|
+
};
|
|
108
|
+
switch (op) {
|
|
109
|
+
case "goto":
|
|
110
|
+
case "open":
|
|
111
|
+
return { op: "goto", url: need("a URL") };
|
|
112
|
+
case "click":
|
|
113
|
+
case "hover":
|
|
114
|
+
case "check":
|
|
115
|
+
case "uncheck":
|
|
116
|
+
return { op, selector: need("a selector") };
|
|
117
|
+
case "fill":
|
|
118
|
+
case "type": {
|
|
119
|
+
const [selector, value] = pair("SELECTOR=VALUE");
|
|
120
|
+
return { op, selector, value };
|
|
121
|
+
}
|
|
122
|
+
case "select": {
|
|
123
|
+
const [selector, value] = pair("SELECTOR=VALUE");
|
|
124
|
+
return { op: "select", selector, value };
|
|
125
|
+
}
|
|
126
|
+
case "press": {
|
|
127
|
+
const [sel, val] = splitSelectorValue(need("a key"));
|
|
128
|
+
return val === undefined ? { op: "press", key: sel } : { op: "press", selector: sel, key: val };
|
|
129
|
+
}
|
|
130
|
+
case "scroll":
|
|
131
|
+
return { op: "scroll", to: need("top|bottom|up|down|SELECTOR") };
|
|
132
|
+
case "wait": {
|
|
133
|
+
if (/^\d+$/.test(rest))
|
|
134
|
+
return { op: "wait", ms: Number(rest) };
|
|
135
|
+
return { op: "wait_for", selector: need("milliseconds or a selector") };
|
|
136
|
+
}
|
|
137
|
+
case "text":
|
|
138
|
+
return rest ? { op: "text", selector: rest } : { op: "text" };
|
|
139
|
+
case "links":
|
|
140
|
+
return rest ? { op: "links", selector: rest } : { op: "links" };
|
|
141
|
+
case "attr": {
|
|
142
|
+
const at = need("SELECTOR@attribute").lastIndexOf("@");
|
|
143
|
+
if (at < 1)
|
|
144
|
+
throw new Error(`step "${spec}": expected attr:SELECTOR@name`);
|
|
145
|
+
return { op: "attr", selector: rest.slice(0, at), name: rest.slice(at + 1) };
|
|
146
|
+
}
|
|
147
|
+
case "title":
|
|
148
|
+
case "url":
|
|
149
|
+
case "back":
|
|
150
|
+
case "forward":
|
|
151
|
+
case "reload":
|
|
152
|
+
case "close":
|
|
153
|
+
return { op };
|
|
154
|
+
case "screenshot":
|
|
155
|
+
case "shot":
|
|
156
|
+
return rest ? { op: "screenshot", path: rest } : { op: "screenshot" };
|
|
157
|
+
case "expect_text": {
|
|
158
|
+
const [selector, text] = pair("SELECTOR=TEXT");
|
|
159
|
+
return { op: "expect_text", selector, text };
|
|
160
|
+
}
|
|
161
|
+
case "expect_url":
|
|
162
|
+
return { op: "expect_url", contains: need("text the URL contains") };
|
|
163
|
+
default:
|
|
164
|
+
throw new Error(`unknown step "${spec}" (goto, click, fill, type, press, select, check, hover, scroll, wait, text, links, attr, title, url, screenshot, back, forward, reload, expect_text, expect_url)`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// ── runner ──────────────────────────────────────────────────────────────────
|
|
168
|
+
const clip = (s, n = MAX_TEXT) => (s.length > n ? `${s.slice(0, n)}… [truncated ${s.length - n} chars]` : s);
|
|
169
|
+
function screenshotTarget(root, requested) {
|
|
170
|
+
const rel = requested ?? path.join(".xcoder", "screenshots", `shot-${new Date().toISOString().replace(/[:.]/g, "-")}.png`);
|
|
171
|
+
const full = path.resolve(root, rel);
|
|
172
|
+
const base = path.resolve(root);
|
|
173
|
+
if (full !== base && !full.startsWith(base + path.sep))
|
|
174
|
+
throw new Error(`screenshot path "${rel}" is outside the workspace`);
|
|
175
|
+
fs.mkdirSync(path.dirname(full), { recursive: true });
|
|
176
|
+
// Resolve symlinks on the directory so a link inside the workspace can't redirect the write.
|
|
177
|
+
const realDir = fs.realpathSync(path.dirname(full));
|
|
178
|
+
const realBase = fs.realpathSync(base);
|
|
179
|
+
if (realDir !== realBase && !realDir.startsWith(realBase + path.sep))
|
|
180
|
+
throw new Error(`screenshot path "${rel}" resolves outside the workspace`);
|
|
181
|
+
return path.join(realDir, path.basename(full));
|
|
182
|
+
}
|
|
183
|
+
export async function runBrowse(root, input) {
|
|
184
|
+
const results = [];
|
|
185
|
+
const screenshots = [];
|
|
186
|
+
const fail = (detail) => ({ ok: false, detail, steps: results, screenshots });
|
|
187
|
+
const steps = [...(input.url ? [{ op: "goto", url: input.url }] : []), ...(input.steps ?? [])];
|
|
188
|
+
if (steps.length === 0)
|
|
189
|
+
return fail("no steps given");
|
|
190
|
+
let chromium;
|
|
191
|
+
try {
|
|
192
|
+
({ chromium } = await import("playwright"));
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return fail("the `playwright` package isn't installed. Run `npm install` then `npx playwright install chromium`.");
|
|
196
|
+
}
|
|
197
|
+
const sessionName = input.session?.trim() || undefined;
|
|
198
|
+
let session = sessionName ? sessions.get(sessionName) : undefined;
|
|
199
|
+
let created = false;
|
|
200
|
+
if (!session) {
|
|
201
|
+
if (sessionName && sessions.size >= MAX_SESSIONS)
|
|
202
|
+
return fail(`too many open browser sessions (max ${MAX_SESSIONS}); close one with {"op":"close"}`);
|
|
203
|
+
const browser = await chromium.launch({ headless: !input.headed });
|
|
204
|
+
const context = await browser.newContext();
|
|
205
|
+
await context.route("**/*", (route) => {
|
|
206
|
+
const why = checkRequestAllowed(route.request().url(), { navigation: route.request().isNavigationRequest() });
|
|
207
|
+
if (why)
|
|
208
|
+
return route.abort("blockedbyclient");
|
|
209
|
+
return route.continue();
|
|
210
|
+
});
|
|
211
|
+
const page = await context.newPage();
|
|
212
|
+
session = { browser, page };
|
|
213
|
+
created = true;
|
|
214
|
+
if (sessionName)
|
|
215
|
+
sessions.set(sessionName, session);
|
|
216
|
+
}
|
|
217
|
+
const { page } = session;
|
|
218
|
+
page.setDefaultTimeout(input.timeoutMs ?? 10000);
|
|
219
|
+
let ok = true;
|
|
220
|
+
let closeRequested = false;
|
|
221
|
+
const secretRefs = [];
|
|
222
|
+
try {
|
|
223
|
+
for (const step of steps) {
|
|
224
|
+
const label = step.op;
|
|
225
|
+
try {
|
|
226
|
+
switch (step.op) {
|
|
227
|
+
case "goto": {
|
|
228
|
+
const why = checkRequestAllowed(step.url, { navigation: true });
|
|
229
|
+
if (why)
|
|
230
|
+
throw new Error(why);
|
|
231
|
+
const resp = await page.goto(step.url, { waitUntil: "domcontentloaded" });
|
|
232
|
+
results.push({ op: label, ok: true, note: `${step.url} → ${resp ? resp.status() : "no response"}` });
|
|
233
|
+
break;
|
|
234
|
+
}
|
|
235
|
+
case "click":
|
|
236
|
+
case "hover":
|
|
237
|
+
case "check":
|
|
238
|
+
case "uncheck":
|
|
239
|
+
await page[step.op](step.selector);
|
|
240
|
+
results.push({ op: label, ok: true, note: step.selector });
|
|
241
|
+
break;
|
|
242
|
+
case "fill":
|
|
243
|
+
case "type": {
|
|
244
|
+
let value = step.value;
|
|
245
|
+
if (step.credentialRef) {
|
|
246
|
+
if (FORBIDDEN_CREDENTIAL_REFS.test(step.credentialRef))
|
|
247
|
+
throw new Error(`credentialRef "${step.credentialRef}" is not allowed in browse`);
|
|
248
|
+
value = resolveCredential(step.credentialRef);
|
|
249
|
+
secretRefs.push(step.credentialRef);
|
|
250
|
+
}
|
|
251
|
+
if (value === undefined)
|
|
252
|
+
throw new Error(`${step.op} needs "value" or "credentialRef"`);
|
|
253
|
+
if (step.op === "fill")
|
|
254
|
+
await page.fill(step.selector, value);
|
|
255
|
+
else
|
|
256
|
+
await page.type(step.selector, value);
|
|
257
|
+
results.push({ op: label, ok: true, note: `${step.selector}${step.credentialRef ? " (value redacted)" : ""}` });
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "select":
|
|
261
|
+
await page.selectOption(step.selector, step.value);
|
|
262
|
+
results.push({ op: label, ok: true, note: `${step.selector} = ${step.value}` });
|
|
263
|
+
break;
|
|
264
|
+
case "press":
|
|
265
|
+
if (step.selector)
|
|
266
|
+
await page.press(step.selector, step.key);
|
|
267
|
+
else
|
|
268
|
+
await page.keyboard.press(step.key);
|
|
269
|
+
results.push({ op: label, ok: true, note: step.key });
|
|
270
|
+
break;
|
|
271
|
+
case "scroll": {
|
|
272
|
+
if (["top", "bottom", "up", "down"].includes(step.to)) {
|
|
273
|
+
// Browser-side code is passed as a string so this file needs no DOM typings.
|
|
274
|
+
await page.evaluate(`(() => { const to = ${JSON.stringify(step.to)}; const h = document.documentElement.scrollHeight;
|
|
275
|
+
if (to === "top") window.scrollTo(0, 0);
|
|
276
|
+
else if (to === "bottom") window.scrollTo(0, h);
|
|
277
|
+
else window.scrollBy(0, to === "down" ? window.innerHeight * 0.8 : -window.innerHeight * 0.8); })()`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
await page.locator(step.to).first().scrollIntoViewIfNeeded();
|
|
281
|
+
}
|
|
282
|
+
results.push({ op: label, ok: true, note: step.to });
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
case "wait_for":
|
|
286
|
+
await page.waitForSelector(step.selector, { state: step.state ?? "visible", timeout: step.timeoutMs });
|
|
287
|
+
results.push({ op: label, ok: true, note: `${step.selector} ${step.state ?? "visible"}` });
|
|
288
|
+
break;
|
|
289
|
+
case "wait":
|
|
290
|
+
await page.waitForTimeout(Math.min(Math.max(step.ms, 0), 30000));
|
|
291
|
+
results.push({ op: label, ok: true, note: `${step.ms}ms` });
|
|
292
|
+
break;
|
|
293
|
+
case "text": {
|
|
294
|
+
const t = (await page.innerText(step.selector ?? "body")) ?? "";
|
|
295
|
+
results.push({ op: label, ok: true, note: step.selector ?? "body", data: clip(t) });
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case "attr": {
|
|
299
|
+
const v = await page.getAttribute(step.selector, step.name);
|
|
300
|
+
results.push({ op: label, ok: true, note: `${step.selector}@${step.name}`, data: v });
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case "links": {
|
|
304
|
+
const links = (await page.$$eval(`${step.selector ?? "body"} a[href]`, `(as) => as.map((a) => ({ text: (a.innerText || "").trim().slice(0, 80), href: a.href }))`));
|
|
305
|
+
results.push({ op: label, ok: true, note: `${links.length} link(s)`, data: links.slice(0, MAX_LINKS) });
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "title":
|
|
309
|
+
results.push({ op: label, ok: true, note: await page.title() });
|
|
310
|
+
break;
|
|
311
|
+
case "url":
|
|
312
|
+
results.push({ op: label, ok: true, note: page.url() });
|
|
313
|
+
break;
|
|
314
|
+
case "back":
|
|
315
|
+
case "forward":
|
|
316
|
+
case "reload": {
|
|
317
|
+
const fn = step.op === "back" ? "goBack" : step.op === "forward" ? "goForward" : "reload";
|
|
318
|
+
await page[fn]({ waitUntil: "domcontentloaded" });
|
|
319
|
+
results.push({ op: label, ok: true, note: page.url() });
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
case "screenshot": {
|
|
323
|
+
const target = screenshotTarget(root, step.path);
|
|
324
|
+
await page.screenshot({ path: target, fullPage: step.fullPage ?? false });
|
|
325
|
+
const rel = path.relative(root, target) || target;
|
|
326
|
+
screenshots.push(rel);
|
|
327
|
+
results.push({ op: label, ok: true, note: rel });
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
case "expect_text": {
|
|
331
|
+
const content = await page.textContent(step.selector);
|
|
332
|
+
const hit = !!content && content.includes(step.text);
|
|
333
|
+
results.push({ op: label, ok: hit, note: hit ? "matched" : `expected "${step.text}", got "${clip(content ?? "", 200)}"` });
|
|
334
|
+
if (!hit)
|
|
335
|
+
ok = false;
|
|
336
|
+
break;
|
|
337
|
+
}
|
|
338
|
+
case "expect_url": {
|
|
339
|
+
const hit = page.url().includes(step.contains);
|
|
340
|
+
results.push({ op: label, ok: hit, note: hit ? "matched" : `got ${page.url()}` });
|
|
341
|
+
if (!hit)
|
|
342
|
+
ok = false;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
case "close":
|
|
346
|
+
closeRequested = true;
|
|
347
|
+
results.push({ op: label, ok: true, note: "browser closed" });
|
|
348
|
+
break;
|
|
349
|
+
default:
|
|
350
|
+
throw new Error(`unknown op "${step.op}"`);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch (e) {
|
|
354
|
+
ok = false;
|
|
355
|
+
let msg = e.message;
|
|
356
|
+
for (const ref of secretRefs)
|
|
357
|
+
msg = redact(msg, ref);
|
|
358
|
+
results.push({ op: label, ok: false, note: clip(msg, 500) });
|
|
359
|
+
break; // stop at the first hard failure
|
|
360
|
+
}
|
|
361
|
+
if (closeRequested)
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
let pageInfo;
|
|
365
|
+
if (!closeRequested) {
|
|
366
|
+
try {
|
|
367
|
+
pageInfo = {
|
|
368
|
+
url: page.url(),
|
|
369
|
+
title: await page.title(),
|
|
370
|
+
text: clip((await page.innerText("body").catch(() => "")).replace(/\n{3,}/g, "\n\n"), 2000),
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
pageInfo = undefined;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
ok,
|
|
379
|
+
detail: ok ? "all steps completed" : "a step failed — see `steps`",
|
|
380
|
+
steps: results,
|
|
381
|
+
page: pageInfo,
|
|
382
|
+
screenshots,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
const keepOpen = sessionName && !closeRequested && ok;
|
|
387
|
+
if (!keepOpen) {
|
|
388
|
+
if (sessionName)
|
|
389
|
+
sessions.delete(sessionName);
|
|
390
|
+
if (created || sessionName)
|
|
391
|
+
await session.browser.close().catch(() => undefined);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one rule this file exists to enforce: a raw credential must never
|
|
3
|
+
* cross back into the LLM's context, a tool-call log, or the RAO trace.
|
|
4
|
+
*
|
|
5
|
+
* The Sub Agent only ever holds a `credentialRef` — the NAME of an
|
|
6
|
+
* environment variable, not a value. Resolution happens here, at the last
|
|
7
|
+
* possible moment, inside the tool execution that actually needs the
|
|
8
|
+
* secret (the Playwright tool), and the resolved value never gets returned
|
|
9
|
+
* to the caller. If you're tempted to add a `getCredentialValue` export
|
|
10
|
+
* that hands the string back to a caller outside `tools/`, don't — that's
|
|
11
|
+
* the exact mistake this module is here to prevent.
|
|
12
|
+
*/
|
|
13
|
+
export declare class CredentialResolutionError extends Error {
|
|
14
|
+
}
|
|
15
|
+
export declare function resolveCredential(ref: string): string;
|
|
16
|
+
/** For anything that might echo a string containing a resolved secret (logs, RAO trace, errors). */
|
|
17
|
+
export declare function redact(text: string, ref?: string): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one rule this file exists to enforce: a raw credential must never
|
|
3
|
+
* cross back into the LLM's context, a tool-call log, or the RAO trace.
|
|
4
|
+
*
|
|
5
|
+
* The Sub Agent only ever holds a `credentialRef` — the NAME of an
|
|
6
|
+
* environment variable, not a value. Resolution happens here, at the last
|
|
7
|
+
* possible moment, inside the tool execution that actually needs the
|
|
8
|
+
* secret (the Playwright tool), and the resolved value never gets returned
|
|
9
|
+
* to the caller. If you're tempted to add a `getCredentialValue` export
|
|
10
|
+
* that hands the string back to a caller outside `tools/`, don't — that's
|
|
11
|
+
* the exact mistake this module is here to prevent.
|
|
12
|
+
*/
|
|
13
|
+
export class CredentialResolutionError extends Error {
|
|
14
|
+
}
|
|
15
|
+
export function resolveCredential(ref) {
|
|
16
|
+
const value = process.env[ref];
|
|
17
|
+
if (!value) {
|
|
18
|
+
throw new CredentialResolutionError(`credential ref "${ref}" is not set in the environment. Set it before running, e.g. ` +
|
|
19
|
+
`\`${ref}=... xcoder -task "..."\`. Xcoder SDLC never accepts a raw secret as a CLI argument ` +
|
|
20
|
+
`or task input — only a reference to where it lives.`);
|
|
21
|
+
}
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
/** For anything that might echo a string containing a resolved secret (logs, RAO trace, errors). */
|
|
25
|
+
export function redact(text, ref) {
|
|
26
|
+
if (!ref)
|
|
27
|
+
return text;
|
|
28
|
+
const value = process.env[ref];
|
|
29
|
+
if (!value)
|
|
30
|
+
return text;
|
|
31
|
+
return text.split(value).join(`«redacted:${ref}»`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface DependencyGraphResultOk {
|
|
2
|
+
ok: true;
|
|
3
|
+
relPath: string;
|
|
4
|
+
imports: Array<{
|
|
5
|
+
specifier: string;
|
|
6
|
+
resolved: string | null;
|
|
7
|
+
isExternal: boolean;
|
|
8
|
+
}>;
|
|
9
|
+
importers: string[];
|
|
10
|
+
parseMethod: "regex" | "tokenizer" | "none";
|
|
11
|
+
note?: string;
|
|
12
|
+
}
|
|
13
|
+
export type DependencyGraphResult = DependencyGraphResultOk | {
|
|
14
|
+
ok: false;
|
|
15
|
+
code: "file_not_found" | "workspace_escape" | "unsupported";
|
|
16
|
+
error: string;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* get_dependency_graph — imports + importers for one file, best-effort.
|
|
20
|
+
* Never throws: unknown extensions (or unparseable files) return typed
|
|
21
|
+
* `unsupported`, missing files `file_not_found`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getDependencyGraphTool(root: string, relPath: string): Promise<DependencyGraphResult>;
|