@tea-agent/loop-agent 0.43.0-next.1 → 0.43.0-next.11
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/CHANGELOG.md +144 -9
- package/dist/application/dag/run-dag.js +5 -0
- package/dist/application/evaluation/budget.js +19 -1
- package/dist/application/task-lifecycle/advance.js +17 -4
- package/dist/application/task-lifecycle/observe.js +43 -29
- package/dist/application/task-lifecycle/plan-transitions.js +5 -4
- package/dist/build-stamp.json +3 -3
- package/dist/executors/dag-pi-executor.js +1237 -586
- package/dist/executors/pi-executor.js +22 -1
- package/dist/executors/pi-sdk-executor.js +140 -39
- package/dist/executors/shell-executor.js +8 -4
- package/dist/shared/frontend-execution-policy.js +23 -0
- package/dist/task/config-types.js +4 -0
- package/dist/task/source-prepare/ledger-reconciliation.js +2 -2
- package/dist/task/source-prepare/ledger-review.js +6 -9
- package/dist/task/source-prepare/semantic-intake.js +16 -26
- package/dist/task/source-prepare/source-fidelity-pi.js +28 -9
- package/dist/task/source-references.js +48 -23
- package/dist/worker/console/chat/assistant-content.js +23 -2
- package/dist/worker/console/chat/browser-policy.js +113 -0
- package/dist/worker/console/chat/browser-routes.js +140 -0
- package/dist/worker/console/chat/explore-tools.js +13 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/ask-user-question.js +44 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/explore-tools.js +202 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/goal-tools.js +79 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/operator-tools.js +86 -0
- package/dist/worker/console/chat/pi-runtime/custom-tools/todo-write.js +38 -0
- package/dist/worker/console/chat/pi-runtime/inspection-types.js +28 -0
- package/dist/worker/console/chat/pi-runtime/inspection.js +439 -0
- package/dist/worker/console/chat/pi-runtime/sdk-bindings.js +153 -0
- package/dist/worker/console/chat/pi-runtime/sdk-session.js +5 -0
- package/dist/worker/console/chat/pi-runtime.js +207 -1178
- package/dist/worker/console/chat/routes.js +208 -71
- package/dist/worker/console/chat/scm-routes.js +217 -0
- package/dist/worker/console/chat/scm-service.js +283 -0
- package/dist/worker/console/chat/scm-tools.js +111 -0
- package/dist/worker/console/chat/sdd-data-alignment.js +65 -9
- package/dist/worker/console/chat/session-catalog.js +32 -0
- package/dist/worker/console/chat/session-mode-view.js +49 -0
- package/dist/worker/console/chat/session-mode.js +219 -0
- package/dist/worker/console/chat/session-store.js +22 -6
- package/dist/worker/console/chat/shortcuts.js +6 -0
- package/dist/worker/console/chat/terminal-routes.js +216 -0
- package/dist/worker/console/chat/terminal-sessions.js +348 -0
- package/dist/worker/console/chat/terminal-tools.js +211 -0
- package/dist/worker/console/chat/tool-preview.js +11 -0
- package/dist/worker/console/chat/tools.js +2 -0
- package/dist/worker/console/server.js +2 -28
- package/dist/worker/console/static/assets/{abnfDiagram-N423BO3Z-B0Q0nClf.js → abnfDiagram-N423BO3Z-CTSK0xbU.js} +1 -1
- package/dist/worker/console/static/assets/{arc-DCPjC19G.js → arc-_2MfctFP.js} +1 -1
- package/dist/worker/console/static/assets/{architectureDiagram-T3A2C74G-CP5n9jmG.js → architectureDiagram-T3A2C74G-DsDe2wTg.js} +1 -1
- package/dist/worker/console/static/assets/{blockDiagram-VBNYF7ZC-CmaWi_Wg.js → blockDiagram-VBNYF7ZC-Dtz_779U.js} +1 -1
- package/dist/worker/console/static/assets/{c4Diagram-5PPSVZJV-Ct5tcMfZ.js → c4Diagram-5PPSVZJV-LaULi-gw.js} +1 -1
- package/dist/worker/console/static/assets/channel-CqS4yBWT.js +1 -0
- package/dist/worker/console/static/assets/{chunk-2GRJ4B5K-C0osm1Zf.js → chunk-2GRJ4B5K-DuHBqqX8.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-2Q5K7J3B-CDWviED7.js → chunk-2Q5K7J3B-BZgZs1dn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5RXB4S5H-mtSk1-j7.js → chunk-5RXB4S5H-tzwmlIO_.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-5VM5RSS4-D5J9PHC7.js → chunk-5VM5RSS4-CeYrLkSn.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-6Q2QTUOP-Begg4WAa.js → chunk-6Q2QTUOP-C92cgilm.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-GF5L2VYU-sF1AEy7T.js → chunk-GF5L2VYU-XOXpGC0F.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-JWPE2WC7-BqbsWXZb.js → chunk-JWPE2WC7-CGHbJyCH.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-KBJHAD2P-BKJrPcJ6.js → chunk-KBJHAD2P-Bs2-LTzf.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-RYQCIY6F-CcwNMRho.js → chunk-RYQCIY6F-rN7up7E4.js} +1 -1
- package/dist/worker/console/static/assets/{chunk-XXDRQBXY-BEJi3iIs.js → chunk-XXDRQBXY-B_fWjKpR.js} +1 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-DVOxNOq0.js +1 -0
- package/dist/worker/console/static/assets/{cose-bilkent-JH36ORCC-DRLgTvVu.js → cose-bilkent-JH36ORCC-DE3SdJvB.js} +1 -1
- package/dist/worker/console/static/assets/{cynefin-VYW2F7L2-BoAVG3cs.js → cynefin-VYW2F7L2-B-4yuRAX.js} +1 -1
- package/dist/worker/console/static/assets/{cynefinDiagram-MW4NZA55-Bzol06hR.js → cynefinDiagram-MW4NZA55-TiCPFuO8.js} +1 -1
- package/dist/worker/console/static/assets/{dagre-VZM6K2ZE-UmliJM77.js → dagre-VZM6K2ZE-7crzLMzA.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-7IWD3JNH-BWLcJkFo.js → diagram-7IWD3JNH-Cvr-x6Fd.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-B4RE2ZJO-CgApOm-O.js → diagram-B4RE2ZJO-BzcRHUjH.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-LBJQPF4R-Bh3RgTLs.js → diagram-LBJQPF4R-BjkAXilh.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-Q27KOJAE-Dsh-D5nE.js → diagram-Q27KOJAE-BkzyATK7.js} +1 -1
- package/dist/worker/console/static/assets/{diagram-UB23O5K3-Dkbbpcpb.js → diagram-UB23O5K3-rZuTarYP.js} +1 -1
- package/dist/worker/console/static/assets/{ebnfDiagram-BXEA7PRR-3EBaA3t3.js → ebnfDiagram-BXEA7PRR-CAYENaN1.js} +1 -1
- package/dist/worker/console/static/assets/{erDiagram-JOGREHBK-Bq2Dc5ok.js → erDiagram-JOGREHBK-DCEam0Uq.js} +1 -1
- package/dist/worker/console/static/assets/{flowDiagram-UKHOOZJN-De7y55ha.js → flowDiagram-UKHOOZJN-e_dH4wVO.js} +1 -1
- package/dist/worker/console/static/assets/{ganttDiagram-PKOTCBZU-fLiDbQRh.js → ganttDiagram-PKOTCBZU-CvZa2zOK.js} +1 -1
- package/dist/worker/console/static/assets/{gitGraphDiagram-DS77QQ5N-Du617mp1.js → gitGraphDiagram-DS77QQ5N-CbT5TuKp.js} +1 -1
- package/dist/worker/console/static/assets/index-CUPeoE8W.js +468 -0
- package/dist/worker/console/static/assets/index-SAANIK8g.css +1 -0
- package/dist/worker/console/static/assets/{infoDiagram-6WML65LV-BwmlF-XP.js → infoDiagram-6WML65LV-BWzHQa_Z.js} +1 -1
- package/dist/worker/console/static/assets/{ishikawaDiagram-WSZJBQD7-zsGtS59u.js → ishikawaDiagram-WSZJBQD7-BTjlovtU.js} +1 -1
- package/dist/worker/console/static/assets/{journeyDiagram-NVQOT4AX-DTTPTe3f.js → journeyDiagram-NVQOT4AX-DkvQoCc1.js} +1 -1
- package/dist/worker/console/static/assets/{kanban-definition-27J2QSJJ-BkXg9aP-.js → kanban-definition-27J2QSJJ-CHSzGi3W.js} +1 -1
- package/dist/worker/console/static/assets/{linear-7U2ue5IE.js → linear-DZMkTCWH.js} +1 -1
- package/dist/worker/console/static/assets/{mermaid.core-BUuGHmWO.js → mermaid.core-BIMhty7A.js} +5 -5
- package/dist/worker/console/static/assets/{mindmap-definition-FAOFIHXS-7q6fX0Sv.js → mindmap-definition-FAOFIHXS-Dx-tyMl3.js} +1 -1
- package/dist/worker/console/static/assets/{pegDiagram-VL7TDLO6-CkQPJN55.js → pegDiagram-VL7TDLO6-C6G34rbY.js} +1 -1
- package/dist/worker/console/static/assets/{pieDiagram-7S7Q4E2Y-CCal_tfX.js → pieDiagram-7S7Q4E2Y-CHK9XOfm.js} +1 -1
- package/dist/worker/console/static/assets/{quadrantDiagram-CIZ2JOQS-B7bpxyAZ.js → quadrantDiagram-CIZ2JOQS-CDARkI0w.js} +1 -1
- package/dist/worker/console/static/assets/{railroadDiagram-AXF67PYL-CJmww7D-.js → railroadDiagram-AXF67PYL-DiuUe0ZQ.js} +1 -1
- package/dist/worker/console/static/assets/{requirementDiagram-LRYGKXZP-D6ldWJEv.js → requirementDiagram-LRYGKXZP-RE61UR56.js} +1 -1
- package/dist/worker/console/static/assets/{sankeyDiagram-W5VNT64P-VGh33I9e.js → sankeyDiagram-W5VNT64P-KDNLHMEG.js} +1 -1
- package/dist/worker/console/static/assets/{sequenceDiagram-SI44F4Z6-CCkDrIjU.js → sequenceDiagram-SI44F4Z6-CeVgXnD9.js} +1 -1
- package/dist/worker/console/static/assets/{sizeCapture-X5ZJPWSS-NC8f5Otb.js → sizeCapture-X5ZJPWSS-CXwGyjjG.js} +1 -1
- package/dist/worker/console/static/assets/{stateDiagram-OKZ733FA-_92ezZdF.js → stateDiagram-OKZ733FA-BjTfozab.js} +1 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-BXoYsyr2.js +1 -0
- package/dist/worker/console/static/assets/{swimlanes-SLNWSIFB-BdUCGtbP.js → swimlanes-SLNWSIFB-BaXOB8GT.js} +2 -2
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-DfKliS4e.js +8 -0
- package/dist/worker/console/static/assets/{timeline-definition-Z64GVDOM-CdD1H8ia.js → timeline-definition-Z64GVDOM-C3r2hqUU.js} +1 -1
- package/dist/worker/console/static/assets/{vennDiagram-T6HMQDX7-Dkdk3oFo.js → vennDiagram-T6HMQDX7-MWKgl1S9.js} +1 -1
- package/dist/worker/console/static/assets/{wardleyDiagram-T6FBY63Y-CKu2uPPY.js → wardleyDiagram-T6FBY63Y-C9iFcZPk.js} +1 -1
- package/dist/worker/console/static/assets/{xychartDiagram-ELKLHX3M-ClqAnuG8.js → xychartDiagram-ELKLHX3M-DqnM24Jp.js} +1 -1
- package/dist/worker/console/static/index.html +2 -2
- package/dist/worker/console/static-src/operator-chat/active-turn-tail.js +10 -0
- package/dist/worker/console/static-src/operator-chat/chat-link.js +94 -0
- package/dist/worker/console/static-src/operator-chat/chat-sse-events.js +122 -10
- package/dist/worker/console/static-src/operator-chat/compaction-message.js +7 -0
- package/dist/worker/console/static-src/operator-chat/pending-user-message.js +27 -15
- package/dist/worker/console/static-src/operator-chat/process-label.js +41 -0
- package/dist/worker/console/static-src/operator-chat/turn-group-equality.js +13 -0
- package/dist/worker/console/static-src/operator-chat/turn-process-disclosure.js +10 -0
- package/dist/worker/console/static-src/operator-chat/use-searchable-hidden.js +51 -0
- package/dist/worker/console/static-src/operator-chat/useChatSessions.js +44 -24
- package/dist/worker/console/static-src/operator-chat/useChatStream.js +18 -2
- package/dist/worker/console/static-src/operator-chat/useChatThread.js +41 -8
- package/dist/worker/console/static-src/operator-chat/useComposer.js +26 -11
- package/dist/worker/console/static-src/operator-chat/user-turn-anchor-equality.js +21 -0
- package/dist/worker/console/workspace-context.js +15 -1
- package/dist/worker/observe/node-transparency.js +81 -72
- package/dist/worker/observe/routes.js +20 -1
- package/dist/worker/observe/static/dag-inspector-humanize.js +3 -0
- package/dist/worker/observe/static/dom.js +20 -1
- package/dist/worker/observe/static/format-pool.d.ts +2 -0
- package/dist/worker/observe/static/format-pool.js +6 -0
- package/dist/worker/observe/static/format.js +7 -0
- package/dist/worker/observe/static/inspect-workspace.js +34 -7
- package/dist/worker/observe/static/inspector-submission.js +32 -0
- package/dist/worker/observe/static/kpi.js +1 -0
- package/dist/worker/observe/static/relations.js +2 -0
- package/dist/worker/observe/static/router.js +13 -0
- package/dist/worker/observe/static/run-processing.js +2 -0
- package/dist/worker/observe/static/shell-chrome.js +36 -3
- package/dist/worker/observe/static/state.js +35 -2
- package/dist/worker/observe/static/styles.css +263 -39
- package/dist/worker/observe/static/task-failure-labels.d.ts +4 -0
- package/dist/worker/observe/static/task-failure-labels.js +67 -0
- package/dist/worker/observe/static/task-history.js +12 -0
- package/dist/worker/observe/static/views/batch.js +6 -13
- package/dist/worker/observe/static/views/dag-graph.js +50 -3
- package/dist/worker/observe/static/views/dag-inspector.js +742 -268
- package/dist/worker/observe/static/views/dag-trajectory.js +3 -0
- package/dist/worker/observe/static/views/dag.d.ts +6 -0
- package/dist/worker/observe/static/views/dag.js +49 -11
- package/dist/worker/observe/static/views/dags.js +2 -0
- package/dist/worker/observe/static/views/dashboard.js +21 -12
- package/dist/worker/observe/static/views/failures.js +21 -11
- package/dist/worker/observe/static/views/feature.js +11 -29
- package/dist/worker/observe/static/views/pool.js +37 -28
- package/dist/worker/observe/static/views/run.js +48 -5
- package/dist/worker/observe/static/views/session-timeline.js +189 -240
- package/dist/worker/observe/static/views/task.js +81 -62
- package/dist/workflows/dag/budget-enforcement.js +53 -3
- package/dist/workflows/dag/frontend-capacity.js +9 -0
- package/dist/workflows/dag/frontend-closeout.js +1 -1
- package/dist/workflows/dag/frontend-design-policy.js +2 -1
- package/dist/workflows/dag/frontend-durable-tools.js +193 -0
- package/dist/workflows/dag/frontend-execution-groups.js +24 -0
- package/dist/workflows/dag/frontend-implementation-contract.js +13 -0
- package/dist/workflows/dag/frontend-input-projection.js +76 -0
- package/dist/workflows/dag/frontend-plan-completeness.js +186 -0
- package/dist/workflows/dag/frontend-plan-recovery-policy.js +18 -0
- package/dist/workflows/dag/frontend-plan-render.js +10 -3
- package/dist/workflows/dag/frontend-prewrite-gate.js +3 -3
- package/dist/workflows/dag/frontend-recovery-controller.js +7 -7
- package/dist/workflows/dag/frontend-recovery-lineage.js +13 -0
- package/dist/workflows/dag/frontend-recovery-run.js +4 -0
- package/dist/workflows/dag/frontend-review-context.js +2 -1
- package/dist/workflows/dag/frontend-review-scopes.js +139 -0
- package/dist/workflows/dag/frontend-session-budget.js +249 -0
- package/dist/workflows/dag/frontend-shadow-dual-write.js +20 -2
- package/dist/workflows/dag/frontend-shape-facts.js +12 -2
- package/dist/workflows/dag/frontend-shape.js +30 -1
- package/dist/workflows/dag/frontend-test-execution-evidence.js +3 -2
- package/dist/workflows/dag/frontend-typed-event-store.js +11 -0
- package/dist/workflows/dag/frontend-verification-trace.js +1 -1
- package/dist/workflows/dag/init-hybrid.js +65 -167
- package/dist/workflows/dag/node-execution.js +38 -155
- package/dist/workflows/dag/prompt.js +4 -0
- package/dist/workflows/dag/rerun-plan.js +7 -1
- package/dist/workflows/dag/runner.js +26 -1
- package/dist/workflows/dag/types.js +6 -0
- package/docs/operations/README.md +1 -0
- package/docs/templates/frontend-design-contract.md +4 -4
- package/docs/templates/frontend-implementation-contract.schema.json +34 -2
- package/docs/templates/frontend-implementation-dag.json +14 -13
- package/package.json +6 -3
- package/skills/frontend-bounded-implement/SKILL.md +3 -3
- package/skills/frontend-bounded-implement/references/code-standards.md +3 -3
- package/skills/frontend-contract/SKILL.md +2 -1
- package/skills/frontend-contract/references/contract-protocol.md +22 -4
- package/skills/frontend-design-review/SKILL.md +22 -16
- package/skills/frontend-design-review/references/review-checklist.md +7 -7
- package/skills/frontend-plan/SKILL.md +21 -2
- package/skills/frontend-plan/references/decision-contract.md +42 -23
- package/skills/frontend-review/SKILL.md +22 -29
- package/skills/frontend-review/references/review-findings.md +24 -43
- package/skills/frontend-scout/references/scout-evidence.md +4 -0
- package/dist/worker/console/static/assets/channel-DAS07MdS.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-JCYQIIEL-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/classDiagram-v2-OCEON4UE-Cwk-5jiW.js +0 -1
- package/dist/worker/console/static/assets/index-C0O48S_P.js +0 -449
- package/dist/worker/console/static/assets/index-CzKf4U8P.css +0 -1
- package/dist/worker/console/static/assets/stateDiagram-v2-UEYNNEHI-DyJg5gzW.js +0 -1
- package/dist/worker/console/static/assets/swimlanesDiagram-ULZ7WXOC-B1cw_0Uy.js +0 -8
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { observeFrontendSession } from "../../workflows/dag/frontend-session-budget.js";
|
|
1
4
|
/**
|
|
2
5
|
* 生产 Source Fidelity Pi 适配器(AC-PROD-001..006 / AC-001..006)
|
|
3
6
|
*
|
|
@@ -22,7 +25,7 @@ import { resolveDagPiModelConfig } from "../../executors/dag-pi-executor.js";
|
|
|
22
25
|
import { resolveExecutorModelMatrices } from "../../executors/model-routing.js";
|
|
23
26
|
import { listPiSdkAvailableModels } from "../../executors/pi-sdk-executor.js";
|
|
24
27
|
import { loadHarnessManifest } from "../../governance/harness.js";
|
|
25
|
-
import { loadTaskConfig } from "../runtime.js";
|
|
28
|
+
import { getTaskPaths, loadTaskConfig } from "../runtime.js";
|
|
26
29
|
import { buildSemanticIntakeModelQueue, extractJsonObject, } from "./semantic-intake.js";
|
|
27
30
|
import { LEDGER_RECONCILIATION_DEFAULT_BUDGET_CHARS, LedgerReconciliationError, } from "./ledger-reconciliation.js";
|
|
28
31
|
import { LedgerReviewError, } from "./ledger-review.js";
|
|
@@ -66,7 +69,7 @@ const reviewResponseSchema = z.object({
|
|
|
66
69
|
disposition: z.enum(FRAGMENT_DISPOSITIONS).optional(),
|
|
67
70
|
reason: z.string().min(1).optional(),
|
|
68
71
|
})
|
|
69
|
-
.
|
|
72
|
+
.nullish(),
|
|
70
73
|
});
|
|
71
74
|
/**
|
|
72
75
|
* 模型队列构建镜像 semantic-intake.ts:MED→HIGH→LOW(harness executors.pi)+
|
|
@@ -112,7 +115,7 @@ function buildReconciliationPrompt(request) {
|
|
|
112
115
|
`fragment path: ${request.fragment.path}`,
|
|
113
116
|
`fragment headingPath: ${request.fragment.headingPath}`,
|
|
114
117
|
`fragment lineRange: ${JSON.stringify(request.fragment.lineRange)}`,
|
|
115
|
-
`
|
|
118
|
+
`packing hint: ${request.budgetChars} chars; the fragment below is complete and must not be truncated or summarized before deciding`,
|
|
116
119
|
"fragment text:",
|
|
117
120
|
"```markdown",
|
|
118
121
|
request.fragment.text,
|
|
@@ -131,7 +134,7 @@ function buildReviewPrompt(request) {
|
|
|
131
134
|
"- approved: true only when the exclusion/conflict handling is justified.",
|
|
132
135
|
"- fragmentId: MUST equal the requested fragment id exactly (echo the `fragment id:` line verbatim); identity mismatch is rejected.",
|
|
133
136
|
"- reason: recommended; explain the verdict.",
|
|
134
|
-
"- revision:
|
|
137
|
+
"- revision: omit or use null when no revision is proposed; otherwise one bounded revision {disposition, reason}. Never invent requirements or human resolutions.",
|
|
135
138
|
JSON.stringify({
|
|
136
139
|
fragmentId: "REQ-SRC-AB12CD34EF56",
|
|
137
140
|
approved: true,
|
|
@@ -247,7 +250,11 @@ export async function runProductionLedgerReconciliation(request, options) {
|
|
|
247
250
|
let lastFailure = "";
|
|
248
251
|
for (const modelRef of queue) {
|
|
249
252
|
try {
|
|
250
|
-
const result = await
|
|
253
|
+
const result = await observeFrontendSession({
|
|
254
|
+
phase: "source-reconciliation", userMessage: "Reconcile the source fragment into the required JSON only.", sourceDigest: createHash("sha256").update(JSON.stringify(request)).digest("hex"), scopeIds: [request.fragment.id], prompt, model: modelRef, taskId: options.taskId, sink: options.observationSink,
|
|
255
|
+
artifactPath: options.taskId ? path.join(getTaskPaths(options.repoRoot, options.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`) : undefined,
|
|
256
|
+
}, observer => execute({
|
|
257
|
+
onAttemptObservation: observer,
|
|
251
258
|
attachedFiles: [],
|
|
252
259
|
modelConfig: resolveDagPiModelConfig(modelRef),
|
|
253
260
|
prompt,
|
|
@@ -266,10 +273,14 @@ export async function runProductionLedgerReconciliation(request, options) {
|
|
|
266
273
|
return [error instanceof Error ? error.message : String(error)];
|
|
267
274
|
}
|
|
268
275
|
},
|
|
269
|
-
});
|
|
276
|
+
}));
|
|
270
277
|
if (!result.ok) {
|
|
271
278
|
lastFailure =
|
|
272
279
|
`${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
|
|
280
|
+
if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
|
|
281
|
+
lastFailure = `SOURCE_FIDELITY_CAPACITY_EXHAUSTED: refine this complete source fragment without losing conditions; ${lastFailure}`;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
273
284
|
continue;
|
|
274
285
|
}
|
|
275
286
|
try {
|
|
@@ -306,7 +317,11 @@ export async function runProductionLedgerReview(request, options) {
|
|
|
306
317
|
let lastFailure = "";
|
|
307
318
|
for (const modelRef of queue) {
|
|
308
319
|
try {
|
|
309
|
-
const result = await
|
|
320
|
+
const result = await observeFrontendSession({
|
|
321
|
+
phase: "source-review", userMessage: "Review the fragment disposition into the required JSON only.", sourceDigest: createHash("sha256").update(JSON.stringify(request)).digest("hex"), scopeIds: [request.fragment.id], prompt, model: modelRef, taskId: options.taskId, sink: options.observationSink,
|
|
322
|
+
artifactPath: options.taskId ? path.join(getTaskPaths(options.repoRoot, options.taskId).taskDir, "artifacts", "session-budget", `${randomUUID()}.json`) : undefined,
|
|
323
|
+
}, observer => execute({
|
|
324
|
+
onAttemptObservation: observer,
|
|
310
325
|
attachedFiles: [],
|
|
311
326
|
modelConfig: resolveDagPiModelConfig(modelRef),
|
|
312
327
|
prompt,
|
|
@@ -325,10 +340,14 @@ export async function runProductionLedgerReview(request, options) {
|
|
|
325
340
|
return [error instanceof Error ? error.message : String(error)];
|
|
326
341
|
}
|
|
327
342
|
},
|
|
328
|
-
});
|
|
343
|
+
}));
|
|
329
344
|
if (!result.ok) {
|
|
330
345
|
lastFailure =
|
|
331
346
|
`${result.failureCategory}: ${(result.stderr || result.assistantText).slice(0, 2000)}`;
|
|
347
|
+
if (result.stopReason === "length" || ["context-overflow", "context-budget-exhausted"].includes(result.failureCategory)) {
|
|
348
|
+
lastFailure = `SOURCE_FIDELITY_CAPACITY_EXHAUSTED: refine this complete source fragment without losing conditions; ${lastFailure}`;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
332
351
|
continue;
|
|
333
352
|
}
|
|
334
353
|
try {
|
|
@@ -380,5 +399,5 @@ export async function resolveProductionSourceFidelityPi(input) {
|
|
|
380
399
|
}
|
|
381
400
|
if (effectiveTaskKind !== SOURCE_FIDELITY_TASK_KIND)
|
|
382
401
|
return undefined;
|
|
383
|
-
return createProductionSourceFidelityPi({ repoRoot: input.repoRoot });
|
|
402
|
+
return createProductionSourceFidelityPi({ repoRoot: input.repoRoot, taskId: input.taskId });
|
|
384
403
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* source/references/, separate from the derived source/需求.md contract.
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { access, copyFile, mkdir, readdir, readFile, writeFile, } from "node:fs/promises";
|
|
8
|
+
import { access, copyFile, lstat, mkdir, readdir, readFile, writeFile, } from "node:fs/promises";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { getTaskPaths, loadTaskConfig } from "./runtime.js";
|
|
11
11
|
import { writeTaskConfig } from "../infrastructure/harness/task-store.js";
|
|
@@ -87,6 +87,7 @@ export async function materializeTaskReferenceDocs(input) {
|
|
|
87
87
|
const referenceDir = path.join(paths.sourceDir, SOURCE_REFERENCE_DIRECTORY);
|
|
88
88
|
await mkdir(referenceDir, { recursive: true });
|
|
89
89
|
const materialized = [];
|
|
90
|
+
const manifest = await readSourceManifest(paths.sourceDir);
|
|
90
91
|
for (const doc of docs) {
|
|
91
92
|
const absoluteSource = path.isAbsolute(doc.path)
|
|
92
93
|
? doc.path
|
|
@@ -94,7 +95,10 @@ export async function materializeTaskReferenceDocs(input) {
|
|
|
94
95
|
await assertReadableFile(absoluteSource, `referenceDocs entry "${doc.name ?? doc.path}"`);
|
|
95
96
|
const extension = path.extname(absoluteSource) || ".md";
|
|
96
97
|
const baseName = slugifyName(doc.name ?? path.basename(absoluteSource, extension));
|
|
97
|
-
const
|
|
98
|
+
const imported = manifest?.documents.find((entry) => path.basename(entry.materializedPath, path.extname(entry.materializedPath)) === baseName);
|
|
99
|
+
const targetName = imported
|
|
100
|
+
? path.basename(imported.materializedPath)
|
|
101
|
+
: `${baseName}${extension}`;
|
|
98
102
|
const targetPath = path.join(referenceDir, targetName);
|
|
99
103
|
try {
|
|
100
104
|
await access(targetPath);
|
|
@@ -132,14 +136,23 @@ export async function importPrdDocument(input) {
|
|
|
132
136
|
catch {
|
|
133
137
|
// directory may be empty / just created
|
|
134
138
|
}
|
|
135
|
-
//
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
// Search every immutable reference, including suffixed and renamed imports.
|
|
140
|
+
// Roles remain distinct: identical bytes must not erase a constraint/AC role.
|
|
141
|
+
const matching = existingManifest?.documents.find((doc) => doc.role === role && doc.sha256 === hash.sha256 && doc.bytes === hash.bytes);
|
|
142
|
+
if (matching) {
|
|
143
|
+
const candidate = path.resolve(paths.sourceDir, matching.materializedPath);
|
|
144
|
+
if (path.dirname(candidate) !== path.resolve(referenceDir)) {
|
|
145
|
+
throw new Error(`REFERENCE_PATH_UNSAFE: ${matching.materializedPath}`);
|
|
146
|
+
}
|
|
147
|
+
const stats = await lstat(candidate);
|
|
148
|
+
if (!stats.isFile() || stats.isSymbolicLink() ||
|
|
149
|
+
!(await sameHashIfExists(candidate, hash.sha256))) {
|
|
150
|
+
throw new Error(`REFERENCE_HASH_MISMATCH: ${matching.materializedPath}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const targetFileName = matching
|
|
154
|
+
? path.basename(matching.materializedPath)
|
|
155
|
+
: ensureUniqueReferenceName(existingNames, requestedName, extension);
|
|
143
156
|
const targetPath = path.join(referenceDir, targetFileName);
|
|
144
157
|
if (!(await fileExists(targetPath))) {
|
|
145
158
|
await writeFile(targetPath, hash.content);
|
|
@@ -149,7 +162,7 @@ export async function importPrdDocument(input) {
|
|
|
149
162
|
? toPosixRelative(input.repoRoot, absoluteSource)
|
|
150
163
|
: input.filePath.split(path.sep).join("/");
|
|
151
164
|
const materializedRelativePath = `${SOURCE_REFERENCE_DIRECTORY}/${targetFileName}`;
|
|
152
|
-
const document = {
|
|
165
|
+
const document = matching ?? {
|
|
153
166
|
role,
|
|
154
167
|
name: requestedName,
|
|
155
168
|
sourcePath: sourcePathForRecord,
|
|
@@ -176,20 +189,28 @@ export async function importPrdDocument(input) {
|
|
|
176
189
|
: (existingManifest?.updatedAt ?? importedAt),
|
|
177
190
|
documents,
|
|
178
191
|
};
|
|
179
|
-
|
|
192
|
+
if (documentSetChanged)
|
|
193
|
+
await writeSourceManifest(paths.sourceDir, manifest);
|
|
180
194
|
const taskConfig = await loadTaskConfig(input.repoRoot, input.taskId);
|
|
181
195
|
const referenceDocs = [...(taskConfig.referenceDocs ?? [])];
|
|
182
|
-
const
|
|
196
|
+
const listedIndex = referenceDocs.findIndex((doc) => {
|
|
183
197
|
const absolute = path.isAbsolute(doc.path)
|
|
184
198
|
? doc.path
|
|
185
199
|
: path.resolve(input.repoRoot, doc.path);
|
|
186
200
|
return absolute === absoluteSource;
|
|
187
201
|
});
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
202
|
+
// Keep original source paths as provenance, but point materialization at
|
|
203
|
+
// the chosen immutable filename so DAG generation cannot recreate duplicates.
|
|
204
|
+
const reference = {
|
|
205
|
+
name: path.basename(targetFileName, path.extname(targetFileName)),
|
|
206
|
+
path: sourcePathForRecord,
|
|
207
|
+
};
|
|
208
|
+
const configChanged = listedIndex < 0 || referenceDocs[listedIndex]?.name !== reference.name;
|
|
209
|
+
if (configChanged) {
|
|
210
|
+
if (listedIndex < 0)
|
|
211
|
+
referenceDocs.push(reference);
|
|
212
|
+
else
|
|
213
|
+
referenceDocs[listedIndex] = { ...referenceDocs[listedIndex], ...reference };
|
|
193
214
|
await writeTaskConfig(input.repoRoot, input.taskId, {
|
|
194
215
|
...taskConfig,
|
|
195
216
|
referenceDocs,
|
|
@@ -198,11 +219,15 @@ export async function importPrdDocument(input) {
|
|
|
198
219
|
// Managed contracts must bump revision after import-prd mutates manifest/task config.
|
|
199
220
|
// Lazy import avoids circular init with contract/observe.
|
|
200
221
|
const { refreshManagedContractAfterImport } = await import("./contract/import-revision.js");
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
222
|
+
const changed = documentSetChanged || configChanged;
|
|
223
|
+
if (changed) {
|
|
224
|
+
await refreshManagedContractAfterImport({
|
|
225
|
+
repoRoot: input.repoRoot,
|
|
226
|
+
taskId: input.taskId,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
205
229
|
return {
|
|
230
|
+
changed,
|
|
206
231
|
taskId: input.taskId,
|
|
207
232
|
sourcePath: sourcePathForRecord,
|
|
208
233
|
materializedAbsolutePath: targetPath,
|
|
@@ -210,7 +235,7 @@ export async function importPrdDocument(input) {
|
|
|
210
235
|
sha256: hash.sha256,
|
|
211
236
|
bytes: hash.bytes,
|
|
212
237
|
manifestPath: getSourceManifestPath(paths.sourceDir),
|
|
213
|
-
referenceDocs
|
|
238
|
+
referenceDocs,
|
|
214
239
|
};
|
|
215
240
|
}
|
|
216
241
|
async function sameHashIfExists(filePath, sha256) {
|
|
@@ -115,9 +115,30 @@ export function splitFinalAssistantBlocks(blocks, options = {}) {
|
|
|
115
115
|
if (lastProcessIndex === -1) {
|
|
116
116
|
return { answerBlocks: displayable, processBlocks: [] };
|
|
117
117
|
}
|
|
118
|
+
const trailing = displayable.slice(lastProcessIndex + 1);
|
|
119
|
+
if (trailing.length > 0) {
|
|
120
|
+
return {
|
|
121
|
+
answerBlocks: trailing,
|
|
122
|
+
processBlocks: displayable.slice(0, lastProcessIndex + 1),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
// The stage ends on a non-text block (a tool call after the prose, or a final
|
|
126
|
+
// thinking block). Returning an empty answer here hid the whole reply inside
|
|
127
|
+
// the collapsed process subtree; keep the last prose block as the visible
|
|
128
|
+
// answer instead (2026-09-08).
|
|
129
|
+
let lastTextIndex = -1;
|
|
130
|
+
for (let i = displayable.length - 1; i >= 0; i--) {
|
|
131
|
+
if (isFinalAnswerBlock(displayable[i])) {
|
|
132
|
+
lastTextIndex = i;
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (lastTextIndex === -1) {
|
|
137
|
+
return { answerBlocks: [], processBlocks: displayable };
|
|
138
|
+
}
|
|
118
139
|
return {
|
|
119
|
-
answerBlocks: displayable
|
|
120
|
-
processBlocks: displayable.
|
|
140
|
+
answerBlocks: [displayable[lastTextIndex]],
|
|
141
|
+
processBlocks: displayable.filter((_block, index) => index !== lastTextIndex),
|
|
121
142
|
};
|
|
122
143
|
}
|
|
123
144
|
export function joinAssistantText(blocks) {
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure URL policy for the Operator Chat browser panel: normalize user input
|
|
3
|
+
* into an http(s) URL. Dependency-free so it is unit-testable.
|
|
4
|
+
*
|
|
5
|
+
* Only the scheme is gated: javascript:, data:, file: and friends must never
|
|
6
|
+
* reach the iframe. Local addresses (localhost / 127.0.0.1 / ::1 / LAN
|
|
7
|
+
* hosts) are deliberately allowed — the panel is a development console and
|
|
8
|
+
* its users routinely browse local dev servers.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Extract the `frame-ancestors` source list of a Content-Security-Policy
|
|
12
|
+
* header, or undefined when the directive is absent (or empty).
|
|
13
|
+
*/
|
|
14
|
+
export function extractFrameAncestors(csp) {
|
|
15
|
+
if (csp === null)
|
|
16
|
+
return undefined;
|
|
17
|
+
for (const directive of csp.split(";")) {
|
|
18
|
+
const parts = directive.trim().split(/\s+/);
|
|
19
|
+
if (parts[0] === "frame-ancestors") {
|
|
20
|
+
const sources = parts.slice(1).filter((source) => source !== "");
|
|
21
|
+
return sources.length === 0 ? undefined : sources;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Decide whether a site can render inside the panel iframe. The signals are
|
|
28
|
+
* exactly the ones the browser enforces when it refuses an iframe load:
|
|
29
|
+
* X-Frame-Options DENY/SAMEORIGIN, or a frame-ancestors directive that does
|
|
30
|
+
* not allow `*` ('self' means the SITE's own origin — never ours, so it also
|
|
31
|
+
* blocks the panel). A site we could not reach yields 'unknown' and the
|
|
32
|
+
* plain iframe stays.
|
|
33
|
+
*/
|
|
34
|
+
export function embeddabilityOf(probe) {
|
|
35
|
+
if (probe.reachable !== true)
|
|
36
|
+
return "unknown";
|
|
37
|
+
const xfo = probe.xFrameOptions?.trim().toUpperCase();
|
|
38
|
+
if (xfo === "DENY" || xfo === "SAMEORIGIN")
|
|
39
|
+
return "blocked";
|
|
40
|
+
if (probe.frameAncestors !== undefined &&
|
|
41
|
+
!probe.frameAncestors.some((source) => source === "*")) {
|
|
42
|
+
return "blocked";
|
|
43
|
+
}
|
|
44
|
+
return "embeddable";
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Schemes that must never reach the iframe even without `//` (javascript:,
|
|
48
|
+
* data:, file:, ...). Host:port lookalikes (example.com:8080) are NOT here —
|
|
49
|
+
* they parse as hosts below.
|
|
50
|
+
*/
|
|
51
|
+
const FORBIDDEN_SCHEMES = new Set([
|
|
52
|
+
"javascript",
|
|
53
|
+
"data",
|
|
54
|
+
"file",
|
|
55
|
+
"about",
|
|
56
|
+
"vbscript",
|
|
57
|
+
"blob",
|
|
58
|
+
"mailto",
|
|
59
|
+
"tel",
|
|
60
|
+
"ftp",
|
|
61
|
+
"ftps",
|
|
62
|
+
"ws",
|
|
63
|
+
"wss",
|
|
64
|
+
"sftp",
|
|
65
|
+
"ssh",
|
|
66
|
+
"chrome",
|
|
67
|
+
"chrome-extension",
|
|
68
|
+
"moz-extension",
|
|
69
|
+
"edge",
|
|
70
|
+
"opera",
|
|
71
|
+
"resource",
|
|
72
|
+
"view-source",
|
|
73
|
+
]);
|
|
74
|
+
/**
|
|
75
|
+
* Normalize one address-bar input against the navigation policy.
|
|
76
|
+
* @param input raw user text.
|
|
77
|
+
*/
|
|
78
|
+
export function normalizeBrowserUrl(input) {
|
|
79
|
+
const trimmed = input.trim();
|
|
80
|
+
if (trimmed === "")
|
|
81
|
+
return { kind: "invalid" };
|
|
82
|
+
// Distinguish an explicit scheme from a bare host:port. "example.com:8080"
|
|
83
|
+
// would match a naive scheme regex (dots are legal in schemes), so a
|
|
84
|
+
// scheme prefix is only honored when it is http(s) or a known-forbidden
|
|
85
|
+
// scheme; anything else is treated as a host and gets https://.
|
|
86
|
+
const schemeMatch = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed);
|
|
87
|
+
let withScheme;
|
|
88
|
+
if (schemeMatch === null) {
|
|
89
|
+
withScheme = `https://${trimmed}`;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
const scheme = schemeMatch[1]?.toLowerCase() ?? "";
|
|
93
|
+
if (scheme === "http" || scheme === "https")
|
|
94
|
+
withScheme = trimmed;
|
|
95
|
+
else if (FORBIDDEN_SCHEMES.has(scheme))
|
|
96
|
+
return { kind: "blocked", reason: "scheme" };
|
|
97
|
+
else
|
|
98
|
+
withScheme = `https://${trimmed}`;
|
|
99
|
+
}
|
|
100
|
+
let url;
|
|
101
|
+
try {
|
|
102
|
+
url = new URL(withScheme);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return { kind: "invalid" };
|
|
106
|
+
}
|
|
107
|
+
// Protocol backstop: any URL that still parses to a non-http(s) scheme
|
|
108
|
+
// (ftp://, ws:// — which carry `//` and skip the list) is refused here.
|
|
109
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
110
|
+
return { kind: "blocked", reason: "scheme" };
|
|
111
|
+
}
|
|
112
|
+
return { kind: "ok", url: url.href };
|
|
113
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP routes for the Operator Chat browser panel: one read-only probe
|
|
3
|
+
* endpoint the panel uses to explain an iframe refusal (X-Frame-Options /
|
|
4
|
+
* CSP frame-ancestors are exactly the signals the browser enforces when it
|
|
5
|
+
* refuses to embed a site). The probe is display-only (headers back to the
|
|
6
|
+
* caller), restricted to http(s) URLs, with a hard timeout. Local addresses
|
|
7
|
+
* are allowed — the panel routinely browses local dev servers.
|
|
8
|
+
*/
|
|
9
|
+
import { embeddabilityOf, extractFrameAncestors, } from "./browser-policy.js";
|
|
10
|
+
function workspaceIdOf(req) {
|
|
11
|
+
try {
|
|
12
|
+
return new URL(req.url ?? "/", "http://localhost").searchParams.get("workspaceId") ?? undefined;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function sendJson(res, status, body) {
|
|
19
|
+
res.writeHead(status, {
|
|
20
|
+
"content-type": "application/json; charset=utf-8",
|
|
21
|
+
"cache-control": "no-store",
|
|
22
|
+
});
|
|
23
|
+
res.end(JSON.stringify(body));
|
|
24
|
+
}
|
|
25
|
+
/** Route dispatcher; returns true when the request was handled. */
|
|
26
|
+
export async function handleBrowserRequest(req, res, deps, pathname) {
|
|
27
|
+
if (!pathname.startsWith("/api/operator/v1/chat/browser"))
|
|
28
|
+
return false;
|
|
29
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
30
|
+
const rest = pathname.slice("/api/operator/v1/chat/browser".length);
|
|
31
|
+
const parts = rest.split("/").filter(Boolean);
|
|
32
|
+
const sub = parts[0] ?? "";
|
|
33
|
+
try {
|
|
34
|
+
if (method === "GET" && sub === "probe" && parts.length === 1) {
|
|
35
|
+
// Fail-closed: the probe needs a workspaceId like every chat API.
|
|
36
|
+
if (deps.requireWorkspace) {
|
|
37
|
+
const ws = deps.requireWorkspace(req);
|
|
38
|
+
if (!ws.ok) {
|
|
39
|
+
sendJson(res, 400, {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: { code: "WORKSPACE_REQUIRED", message: "workspaceId is required" },
|
|
42
|
+
});
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (!workspaceIdOf(req)) {
|
|
47
|
+
sendJson(res, 400, {
|
|
48
|
+
ok: false,
|
|
49
|
+
error: { code: "WORKSPACE_REQUIRED", message: "workspaceId is required" },
|
|
50
|
+
});
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
54
|
+
const raw = url.searchParams.get("url") ?? "";
|
|
55
|
+
let parsed;
|
|
56
|
+
try {
|
|
57
|
+
parsed = new URL(raw);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
sendJson(res, 400, {
|
|
61
|
+
ok: false,
|
|
62
|
+
error: { code: "INVALID_URL", message: "invalid url" },
|
|
63
|
+
});
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
|
67
|
+
sendJson(res, 400, {
|
|
68
|
+
ok: false,
|
|
69
|
+
error: { code: "BAD_SCHEME", message: "only http/https urls can be probed" },
|
|
70
|
+
});
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
const timer = setTimeout(() => controller.abort(), 8000);
|
|
75
|
+
try {
|
|
76
|
+
let response = await fetch(parsed, {
|
|
77
|
+
method: "HEAD",
|
|
78
|
+
redirect: "follow",
|
|
79
|
+
signal: controller.signal,
|
|
80
|
+
});
|
|
81
|
+
// Some servers answer HEAD with 405/501; retry once as GET
|
|
82
|
+
// (the body is discarded — only the headers matter).
|
|
83
|
+
if (response.status === 405 || response.status === 501) {
|
|
84
|
+
response = await fetch(parsed, {
|
|
85
|
+
method: "GET",
|
|
86
|
+
redirect: "follow",
|
|
87
|
+
signal: controller.signal,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
const csp = response.headers.get("content-security-policy");
|
|
91
|
+
const frameAncestors = extractFrameAncestors(csp);
|
|
92
|
+
const xFrameOptions = response.headers.get("x-frame-options");
|
|
93
|
+
const probe = {
|
|
94
|
+
reachable: true,
|
|
95
|
+
url: response.url,
|
|
96
|
+
status: response.status,
|
|
97
|
+
...(xFrameOptions !== null ? { xFrameOptions } : {}),
|
|
98
|
+
...(frameAncestors !== undefined ? { frameAncestors } : {}),
|
|
99
|
+
};
|
|
100
|
+
sendJson(res, 200, {
|
|
101
|
+
ok: true,
|
|
102
|
+
data: { probe, verdict: embeddabilityOf(probe) },
|
|
103
|
+
});
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// DNS / TLS / connection / timeout: nothing to judge — the
|
|
108
|
+
// client keeps the plain iframe.
|
|
109
|
+
sendJson(res, 200, {
|
|
110
|
+
ok: true,
|
|
111
|
+
data: { probe: { reachable: false }, verdict: "unknown" },
|
|
112
|
+
});
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
sendJson(res, 404, {
|
|
120
|
+
ok: false,
|
|
121
|
+
error: { code: "NOT_FOUND", message: `unknown browser route: ${method} ${pathname}` },
|
|
122
|
+
});
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (!res.headersSent) {
|
|
127
|
+
sendJson(res, 500, {
|
|
128
|
+
ok: false,
|
|
129
|
+
error: {
|
|
130
|
+
code: "BROWSER_INTERNAL",
|
|
131
|
+
message: error instanceof Error ? error.message : String(error),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
res.end();
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -71,6 +71,10 @@ export const SECRET_SCRUB_PATTERNS = [
|
|
|
71
71
|
name: "private-key-block",
|
|
72
72
|
re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----/g,
|
|
73
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: "aimax-auth-token",
|
|
76
|
+
re: /\bAIMAX_AUTH_TOKEN=["']?[^\s"']+/g,
|
|
77
|
+
},
|
|
74
78
|
];
|
|
75
79
|
/** Throw a typed explore error (kept as a value object so callers can read .code). */
|
|
76
80
|
export class ExploreDeniedError extends Error {
|
|
@@ -89,6 +93,10 @@ export class ExploreDeniedError extends Error {
|
|
|
89
93
|
*/
|
|
90
94
|
export function scrubSecrets(input) {
|
|
91
95
|
let out = input;
|
|
96
|
+
const liveToken = process.env.AIMAX_AUTH_TOKEN;
|
|
97
|
+
if (liveToken && liveToken.length >= 8 && out.includes(liveToken)) {
|
|
98
|
+
out = out.split(liveToken).join("[REDACTED:aimax-auth-token]");
|
|
99
|
+
}
|
|
92
100
|
const redactions = [];
|
|
93
101
|
for (const { name, re } of SECRET_SCRUB_PATTERNS) {
|
|
94
102
|
re.lastIndex = 0;
|
|
@@ -98,6 +106,11 @@ export function scrubSecrets(input) {
|
|
|
98
106
|
redactions.push({ name, count: matches.length });
|
|
99
107
|
}
|
|
100
108
|
}
|
|
109
|
+
if (liveToken && liveToken.length >= 8 && input.includes(liveToken)) {
|
|
110
|
+
const existing = redactions.find((entry) => entry.name === "aimax-auth-token");
|
|
111
|
+
if (!existing)
|
|
112
|
+
redactions.push({ name: "aimax-auth-token", count: 1 });
|
|
113
|
+
}
|
|
101
114
|
return { scrubbed: out, redactions };
|
|
102
115
|
}
|
|
103
116
|
/** True if a repo-relative path matches a sensitive-file denylist pattern. */
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-bound ask_user_question custom tool.
|
|
3
|
+
* ConsolePiRuntime supplies the sessionId and UserQuestionRegistry.
|
|
4
|
+
*/
|
|
5
|
+
const ASK_USER_QUESTION_DESCRIPTION = "Ask the user concise questions when a confirmation, choice, or missing detail is required before proceeding. " +
|
|
6
|
+
"The tool pauses until the user answers. Use it instead of writing questions as ordinary assistant text. " +
|
|
7
|
+
"Send one or more questions with stable ids; recommended options should be first and end with (Recommended).";
|
|
8
|
+
export async function buildAskUserQuestionTool(options) {
|
|
9
|
+
const [{ Type }, { defineTool }] = await Promise.all([
|
|
10
|
+
import("typebox"),
|
|
11
|
+
import("@earendil-works/pi-coding-agent"),
|
|
12
|
+
]);
|
|
13
|
+
return defineTool({
|
|
14
|
+
name: "ask_user_question",
|
|
15
|
+
label: "Ask user",
|
|
16
|
+
description: ASK_USER_QUESTION_DESCRIPTION,
|
|
17
|
+
promptSnippet: ASK_USER_QUESTION_DESCRIPTION,
|
|
18
|
+
parameters: Type.Object({
|
|
19
|
+
questions: Type.Array(Type.Object({
|
|
20
|
+
id: Type.String({ description: "Stable question id echoed in the answer." }),
|
|
21
|
+
question: Type.String({ description: "Specific user-facing question." }),
|
|
22
|
+
header: Type.Optional(Type.String({ description: "Optional short heading." })),
|
|
23
|
+
detail: Type.Optional(Type.String({ description: "Optional Markdown context shown below the question." })),
|
|
24
|
+
options: Type.Optional(Type.Array(Type.Object({
|
|
25
|
+
label: Type.String({ description: "Short user-facing option label." }),
|
|
26
|
+
description: Type.Optional(Type.String({ description: "One sentence explaining impact or tradeoff." })),
|
|
27
|
+
}, { additionalProperties: false }))),
|
|
28
|
+
multi_select: Type.Optional(Type.Boolean({ description: "Allow more than one option." })),
|
|
29
|
+
}, { additionalProperties: false }), { minItems: 1, maxItems: 20 }),
|
|
30
|
+
}, { additionalProperties: false }),
|
|
31
|
+
async execute(toolCallId, params, signal) {
|
|
32
|
+
const answer = await options.registry.ask({
|
|
33
|
+
sessionId: options.sessionId,
|
|
34
|
+
toolCallId,
|
|
35
|
+
questions: params.questions,
|
|
36
|
+
signal,
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
content: [{ type: "text", text: JSON.stringify(answer) }],
|
|
40
|
+
details: answer,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|