@iowarp/clio-coder 0.3.7 → 0.3.8
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 +31 -0
- package/README.md +7 -2
- package/dist/{acp-SK4MD6MM.js → acp-U67UHUK2.js} +13 -13
- package/dist/{agents-2FN2K6ME.js → agents-YU6SGALZ.js} +35 -33
- package/dist/assets/codewiki.json +1 -1
- package/dist/{auth-QIYZWM5I.js → auth-5ZPJOIVG.js} +32 -24
- package/dist/builtins-C6JMZVV6.js +17 -0
- package/dist/{chunk-YD734TPH.js → chunk-26LEYJZH.js} +2 -2
- package/dist/{chunk-TSHXZTOQ.js → chunk-2HEJ2F35.js} +5 -5
- package/dist/{chunk-WJHBC77E.js → chunk-2HFZQUHL.js} +5 -5
- package/dist/{chunk-FO5ZOVUY.js → chunk-3BINW3FP.js} +5 -5
- package/dist/{chunk-EOOQZZDE.js → chunk-4SPRNWDE.js} +18 -18
- package/dist/{chunk-EBEFWSGL.js → chunk-5DHKRSMQ.js} +7 -7
- package/dist/{chunk-OBMAI2DP.js → chunk-5H3GB5BO.js} +11 -9
- package/dist/{chunk-JTSEDYVQ.js → chunk-5Q2VVUKB.js} +7 -7
- package/dist/{chunk-VQNODYQ4.js → chunk-7RFXX52T.js} +294 -3704
- package/dist/{chunk-XEGB6BCN.js → chunk-7RGZWPB6.js} +5 -5
- package/dist/{chunk-GH5622CP.js → chunk-A2NJGIB3.js} +2 -2
- package/dist/{chunk-CEYBNUGC.js → chunk-A3WNZD3P.js} +323 -27
- package/dist/{chunk-D4MDIG46.js → chunk-B5CSFE7B.js} +7 -7
- package/dist/{chunk-2SFS6XQE.js → chunk-DGSYXYMX.js} +3 -2
- package/dist/{chunk-ZWLZP4ZT.js → chunk-DYIM5TJT.js} +98 -12
- package/dist/{chunk-KCMKRQX4.js → chunk-E77JEWSD.js} +42 -49
- package/dist/{chunk-J7PIKKWC.js → chunk-EMYUUSFG.js} +7 -7
- package/dist/{chunk-JRIO5UD2.js → chunk-EQ63NRB7.js} +5 -5
- package/dist/{chunk-UVDSQ6LW.js → chunk-FBVTI2TJ.js} +74 -28
- package/dist/{chunk-BMWK7ZIZ.js → chunk-FHJEP5SW.js} +13 -13
- package/dist/{chunk-FOT2FX5J.js → chunk-GN57SG4G.js} +7 -7
- package/dist/{chunk-SROCI7ZU.js → chunk-GPIEI3LY.js} +5 -5
- package/dist/{chunk-UFQ3F4FW.js → chunk-GU2UIAFZ.js} +4 -4
- package/dist/{chunk-UND3GU2L.js → chunk-H7IXIC72.js} +2 -2
- package/dist/{chunk-VREKEFLL.js → chunk-HLE42MG7.js} +3 -3
- package/dist/{chunk-465YSENW.js → chunk-IFBNV6H6.js} +3 -3
- package/dist/{chunk-LADCF22A.js → chunk-IGWKHNIQ.js} +113 -54
- package/dist/chunk-IIZWH4XA.js +172 -0
- package/dist/{chunk-UHXRNZ2J.js → chunk-IJ7RPIYJ.js} +4 -4
- package/dist/{chunk-M4AKACEO.js → chunk-J3YUBZWY.js} +2 -2
- package/dist/{chunk-AB4XIIVB.js → chunk-JOZYP4GM.js} +6 -6
- package/dist/{chunk-ZGH7FGS5.js → chunk-K4XHGFR5.js} +742 -12
- package/dist/{chunk-EELBMBT6.js → chunk-KTYTFRMB.js} +62 -5
- package/dist/chunk-LU7P4LHA.js +33 -0
- package/dist/{chunk-DQA7QLMD.js → chunk-ME6CCNFO.js} +9 -24
- package/dist/{chunk-KZ2H5X4G.js → chunk-MXKJU4JB.js} +88 -14
- package/dist/{chunk-DMD2AGVS.js → chunk-N22QMJKY.js} +20 -18
- package/dist/chunk-NMPKI6XL.js +3006 -0
- package/dist/chunk-NUGM5KR6.js +165 -0
- package/dist/{chunk-5C77SEEY.js → chunk-P43ETTHK.js} +2 -2
- package/dist/{chunk-JNXPYBB4.js → chunk-PMDBGQSJ.js} +2 -2
- package/dist/chunk-PT7HYKEM.js +165 -0
- package/dist/{chunk-GEYXPTRF.js → chunk-RWSI4YD7.js} +2 -2
- package/dist/{chunk-C4JBQ5SR.js → chunk-TANS5ZJS.js} +6 -6
- package/dist/{chunk-YTYFXUI3.js → chunk-TB5666IT.js} +9 -9
- package/dist/{chunk-UUANF5CR.js → chunk-TLQJPP24.js} +375 -687
- package/dist/{chunk-OB5HIGJY.js → chunk-TT36MB5S.js} +2 -1
- package/dist/{chunk-DJNLUABN.js → chunk-TTHACPOM.js} +135 -17
- package/dist/{chunk-6M7VS3J3.js → chunk-TYPGUK6W.js} +4 -4
- package/dist/{chunk-THKY7CD7.js → chunk-U6MBIEMB.js} +108 -24
- package/dist/{chunk-PD3MESLB.js → chunk-VAWNZU7Z.js} +4 -4
- package/dist/{chunk-IR4CFBFN.js → chunk-VCBR6CU7.js} +12 -12
- package/dist/{chunk-WHJYKASB.js → chunk-VHN4MY6O.js} +2 -2
- package/dist/{chunk-MXI6J5JF.js → chunk-VWZOAB7K.js} +10 -10
- package/dist/{chunk-QCTRSGHQ.js → chunk-WLFILSD5.js} +36 -36
- package/dist/{chunk-5UJ6ECTS.js → chunk-WNIJTQQK.js} +6 -6
- package/dist/{chunk-4DWFMQDR.js → chunk-WSB3FPX7.js} +68 -20
- package/dist/{chunk-3HAPLH5M.js → chunk-WWCZ5F23.js} +112 -10
- package/dist/{chunk-X2KV5FXT.js → chunk-WXY7KU3G.js} +2 -2
- package/dist/{chunk-PPAMZ32Z.js → chunk-XK56QHLX.js} +6 -1
- package/dist/{chunk-DOOEX22V.js → chunk-XWSF374K.js} +4 -4
- package/dist/{chunk-6TUKSZVF.js → chunk-YS5VLNH5.js} +8 -8
- package/dist/{chunk-ZZMN5OM4.js → chunk-ZNLWCMVZ.js} +2 -2
- package/dist/{chunk-WHGPSPT5.js → chunk-ZVJ5BLO2.js} +2 -2
- package/dist/cli/index.js +31 -31
- package/dist/{clio-WBVQEBKO.js → clio-QVTYJ57A.js} +8 -8
- package/dist/code-nav-FGGFIE7L.js +2 -2
- package/dist/codewiki/build-worker.js +4 -4
- package/dist/{components-F7OEATSO.js → components-ZFA3SAER.js} +8 -8
- package/dist/{config-TRBL3RCF.js → config-LW5IJFQN.js} +58 -53
- package/dist/{configure-OLCVPHNM.js → configure-7XIZCOU4.js} +26 -21
- package/dist/{context-XEWE3MOJ.js → context-L3WL3X7K.js} +47 -41
- package/dist/{context-WFPKQSM6.js → context-N52ZA626.js} +22 -22
- package/dist/{context-MJIJ6GOX.js → context-Y6Y7QPR6.js} +12 -12
- package/dist/{context-clear-KNOS2JPB.js → context-clear-MBQRLSDQ.js} +47 -41
- package/dist/{context-index-SSR5ECNE.js → context-index-HVMFQHK3.js} +5 -5
- package/dist/{context-working-set-EUXAZI6N.js → context-working-set-GS6DSO7F.js} +14 -14
- package/dist/{dispatch-runner-B7MTOVKL.js → dispatch-runner-22ZCNOM3.js} +83 -55
- package/dist/{docs-FLJTIDSE.js → docs-7LQ23DLM.js} +8 -8
- package/dist/{doctor-RN4YKO2X.js → doctor-M7YEDGAE.js} +24 -20
- package/dist/{eval-RUBJVSNQ.js → eval-BEC2WHDA.js} +71 -19
- package/dist/{evidence-JZNBUOQZ.js → evidence-REJUMSKM.js} +62 -56
- package/dist/{evolve-FJVC4KKI.js → evolve-PY5ZBA5K.js} +41 -35
- package/dist/{extensions-IQL36S7K.js → extensions-HVKU65YU.js} +6 -6
- package/dist/{fleet-BDKYJFCP.js → fleet-7WZEWRFA.js} +72 -67
- package/dist/{fleet-commands-ZFIWZSB3.js → fleet-commands-UVHWM76J.js} +10 -10
- package/dist/{fleet-graph-Y6HPXIVF.js → fleet-graph-6ULH7PES.js} +13 -13
- package/dist/{fleet-preflight-BHSNPBMH.js → fleet-preflight-J53T6CCE.js} +5 -5
- package/dist/{fleet-validate-BIYREGIK.js → fleet-validate-72PC4SLA.js} +17 -17
- package/dist/{init-LQUB5COQ.js → init-OG3TPGQG.js} +61 -56
- package/dist/{library-NJAHIGG4.js → library-CNTMPLRF.js} +18 -18
- package/dist/{memory-OG6HOYKM.js → memory-6IS7F275.js} +43 -37
- package/dist/{models-5ZG5XY7J.js → models-ENRJDA5W.js} +33 -28
- package/dist/{monitor-TJ7AMTGB.js → monitor-XLDVO7TN.js} +56 -38
- package/dist/{orchestrator-WZYB54DM.js → orchestrator-6KSPYRHA.js} +421 -303
- package/dist/{paths-XUC7GS6E.js → paths-DBXMZMDU.js} +5 -5
- package/dist/registry-LG64LTF4.js +11 -0
- package/dist/{reset-PXQT45IY.js → reset-RZ4ER727.js} +10 -10
- package/dist/{run-FQ74YF62.js → run-Y2CNK5RU.js} +79 -74
- package/dist/{share-FW7SVCL3.js → share-A55GYP6Z.js} +16 -16
- package/dist/{skills-7E7IRB3R.js → skills-ALC5J6AT.js} +19 -19
- package/dist/{skills-eval-LI75W6OK.js → skills-eval-JPBEBYQU.js} +52 -47
- package/dist/support-MIETYA5E.js +38 -0
- package/dist/{targets-4CIFKCTW.js → targets-VGNXIR3S.js} +42 -33
- package/dist/{terminal-lease-WUZY7ZV5.js → terminal-lease-WOBR64YA.js} +3 -3
- package/dist/{uninstall-7FV7IP4E.js → uninstall-ZJF5H5ZN.js} +8 -8
- package/dist/{upgrade-K2HVIVMQ.js → upgrade-FUSUAGHR.js} +27 -25
- package/dist/{usage-GTZELZQX.js → usage-N4MKVHKD.js} +51 -46
- package/dist/{verifiers-RLAHT27O.js → verifiers-YAWOJ3H2.js} +13 -13
- package/dist/{verify-BX3BRKH5.js → verify-LTDHYBGY.js} +9 -9
- package/dist/{wiki-generate-ASIFASCN.js → wiki-generate-6M7GHTBJ.js} +66 -61
- package/dist/worker/entry.js +68 -61
- package/docs/alcf-provider.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/artifact-versions.md +6 -5
- package/docs/built-in-agents.md +1 -1
- package/docs/commands-and-modes.md +2 -2
- package/docs/configuration-and-targets.md +8 -3
- package/docs/context-engine.md +1 -1
- package/docs/documentation-guide.md +1 -1
- package/docs/eval-runner.md +1 -1
- package/docs/evals-internal.md +1 -1
- package/docs/evidence-and-memory.md +70 -6
- package/docs/evolution.md +1 -1
- package/docs/extensions-and-sharing.md +1 -1
- package/docs/fleet-dispatch.md +34 -9
- package/docs/glossary.md +21 -1
- package/docs/installation-and-lifecycle.md +1 -1
- package/docs/middleware-and-components.md +1 -1
- package/docs/model-catalog.md +1 -1
- package/docs/observability.md +2 -2
- package/docs/proactive-memory.md +1 -1
- package/docs/prompt-envelope-and-tools.md +4 -2
- package/docs/provider-adapter-cookbook.md +1 -1
- package/docs/release-cut-checklist.md +41 -38
- package/docs/safety-model.md +1 -1
- package/docs/scientific-validation.md +1 -1
- package/docs/skills-marketplace.md +1 -1
- package/docs/tool-usage.md +1 -1
- package/docs/trace-store.md +1 -1
- package/docs/tui-design.md +1 -1
- package/docs/worker-dispatch-mechanics.md +1 -1
- package/package.json +1 -2
- package/src/cli/argv.ts +5 -0
- package/src/cli/configure.ts +107 -23
- package/src/cli/doctor.ts +5 -1
- package/src/cli/evidence.ts +30 -25
- package/src/cli/fleet-preflight.ts +2 -12
- package/src/cli/shared.ts +1 -0
- package/src/cli/targets.ts +4 -1
- package/src/cli/validate-model.ts +60 -5
- package/src/core/bus-events.ts +25 -0
- package/src/core/commit-attribution.ts +4 -4
- package/src/core/path-boundary.ts +100 -0
- package/src/domains/agents/extension.ts +2 -11
- package/src/domains/agents/fleet-contract.ts +30 -12
- package/src/domains/agents/recipe.ts +7 -1
- package/src/domains/agents/registry.ts +73 -5
- package/src/domains/agents/result-contract.ts +128 -17
- package/src/domains/agents/write-boundary.ts +15 -50
- package/src/domains/context/project-rules.ts +51 -1
- package/src/domains/dispatch/assignment-reconcile.ts +22 -5
- package/src/domains/dispatch/assignment-store.ts +151 -14
- package/src/domains/dispatch/contract.ts +15 -1
- package/src/domains/dispatch/delegation-plan.ts +2 -5
- package/src/domains/dispatch/execution-role.ts +9 -1
- package/src/domains/dispatch/extension.ts +156 -67
- package/src/domains/dispatch/fleet-run.ts +57 -3
- package/src/domains/dispatch/gate-role-prompts.ts +38 -0
- package/src/domains/dispatch/index.ts +3 -1
- package/src/domains/dispatch/intent-requirements.ts +40 -0
- package/src/domains/dispatch/intent.ts +84 -8
- package/src/domains/dispatch/path-scope.ts +370 -0
- package/src/domains/dispatch/receipt-integrity.ts +2 -1
- package/src/domains/dispatch/types.ts +14 -7
- package/src/domains/dispatch/validation.ts +6 -3
- package/src/domains/dispatch/write-boundary-enforcer.ts +45 -0
- package/src/domains/dispatch/write-boundary.ts +201 -22
- package/src/domains/eval/metrics/evidence.ts +79 -2
- package/src/domains/eval/runners/clio-run.ts +12 -2
- package/src/domains/evidence/build.ts +69 -11
- package/src/domains/evidence/index.ts +21 -0
- package/src/domains/evidence/provenance.ts +46 -11
- package/src/domains/evidence/trust-projection.ts +274 -0
- package/src/domains/evidence/trust-status.ts +145 -17
- package/src/domains/evidence/types.ts +4 -0
- package/src/domains/extensions/discovery.ts +88 -1
- package/src/domains/extensions/resources.ts +20 -8
- package/src/domains/extensions/state.ts +6 -2
- package/src/domains/extensions/types.ts +4 -1
- package/src/domains/lifecycle/doctor.ts +140 -1
- package/src/domains/prompts/contract.ts +3 -5
- package/src/domains/providers/extension.ts +30 -2
- package/src/domains/resources/common-loader.ts +3 -0
- package/src/domains/resources/prompts/loader.ts +119 -14
- package/src/domains/safety/policy-engine.ts +5 -5
- package/src/domains/safety/run-effects.ts +64 -1
- package/src/domains/safety/scope.ts +7 -12
- package/src/engine/acp/server.ts +4 -1
- package/src/engine/prompt-templates.ts +18 -1
- package/src/engine/worker-runtime.ts +6 -3
- package/src/interactive/dispatch-board.ts +53 -3
- package/src/interactive/interactive-event-projection.ts +14 -0
- package/src/interactive/overlays/settings.ts +141 -47
- package/src/interactive/slash-commands.ts +7 -2
- package/src/interactive/view/artifacts.ts +42 -9
- package/src/interactive/view/view-overlay.ts +15 -3
- package/src/interactive/worker-receipts.ts +14 -2
- package/src/interactive/worker-stream.ts +8 -0
- package/src/tools/dispatch-admission.ts +12 -13
- package/src/tools/dispatch-arguments.ts +27 -0
- package/src/tools/dispatch-plan.ts +29 -0
- package/src/tools/dispatch-runner.ts +48 -13
- package/src/tools/monitor.ts +13 -0
- package/src/tools/worker-evidence.ts +19 -13
- package/src/worker/spec-contract.ts +2 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GateDecisionArtifact, GateDecisionVerification } from "../dispatch/gate-decisions.js";
|
|
2
|
-
import {
|
|
2
|
+
import { RUN_RECEIPT_INTEGRITY_VERSION, verifyReceiptIntegrity } from "../dispatch/receipt-integrity.js";
|
|
3
3
|
import type {
|
|
4
4
|
RunBriefingProvenance,
|
|
5
5
|
RunEnvelope,
|
|
@@ -459,13 +459,89 @@ function compatibilitySource(receipt: PersistedRunReceiptTrustFacts, field: stri
|
|
|
459
459
|
const DISPATCH_AUTHORITY: TrustStatusAuthority = { kind: "clio", id: "dispatch" };
|
|
460
460
|
const COMPATIBILITY_AUTHORITY: TrustStatusAuthority = { kind: "unknown", id: "historical-persisted-format" };
|
|
461
461
|
|
|
462
|
+
/**
|
|
463
|
+
* A receipt sealed under an integrity version this build no longer verifies.
|
|
464
|
+
* It is not migrated and not read as evidence, on the same terms as every
|
|
465
|
+
* prior integrity bump, but it is not a tampered receipt either: nothing was
|
|
466
|
+
* checked, so nothing failed. The outcome names both versions so an operator
|
|
467
|
+
* with a store full of last release's runs can tell a retired seal from a
|
|
468
|
+
* broken one.
|
|
469
|
+
*/
|
|
470
|
+
export interface RetiredReceiptIntegrity {
|
|
471
|
+
receiptVersion: number;
|
|
472
|
+
supportedVersion: number;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* The dispatch verifier's result, widened with the retired-format diagnosis
|
|
477
|
+
* this module puts in front of it. A failure without `retired` is the
|
|
478
|
+
* verifier's own verdict: a malformed seal, a ledger mismatch, or a digest
|
|
479
|
+
* that does not match the receipt's contents.
|
|
480
|
+
*/
|
|
481
|
+
export type ReceiptIntegrityOutcome = { ok: true } | { ok: false; reason: string; retired?: RetiredReceiptIntegrity };
|
|
482
|
+
|
|
483
|
+
/** The retired-version diagnosis for a receipt's integrity block, or null when the block is current or unreadable. */
|
|
484
|
+
export function retiredReceiptIntegrity(integrity: unknown): RetiredReceiptIntegrity | null {
|
|
485
|
+
if (!isRecord(integrity)) return null;
|
|
486
|
+
const version = integrity.version;
|
|
487
|
+
if (typeof version !== "number" || !Number.isInteger(version) || version < 1) return null;
|
|
488
|
+
if (version >= RUN_RECEIPT_INTEGRITY_VERSION) return null;
|
|
489
|
+
return { receiptVersion: version, supportedVersion: RUN_RECEIPT_INTEGRITY_VERSION };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** The one sentence every surface prints for a retired seal. It never calls the receipt invalid, broken, or corrupt. */
|
|
493
|
+
export function retiredReceiptIntegrityReason(retired: RetiredReceiptIntegrity): string {
|
|
494
|
+
return `receipt integrity v${retired.receiptVersion} is retired; this build verifies v${retired.supportedVersion}; the receipt is not read as evidence`;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Verify a receipt against its ledger row, diagnosing a retired seal before
|
|
499
|
+
* the verifier can report it as a malformed one. Every trust-bearing
|
|
500
|
+
* verification goes through here so the CLI, the bundle, and the TUI receipt
|
|
501
|
+
* view agree on what a retired receipt is.
|
|
502
|
+
*/
|
|
503
|
+
export function verifyReceiptIntegrityOutcome(receipt: RunReceipt, envelope: RunEnvelope): ReceiptIntegrityOutcome {
|
|
504
|
+
const retired = retiredReceiptIntegrity(receipt.integrity);
|
|
505
|
+
if (retired !== null) return { ok: false, reason: retiredReceiptIntegrityReason(retired), retired };
|
|
506
|
+
return verifyReceiptIntegrity(receipt, envelope);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const RETIRED_INTEGRITY_SOURCE = /:integrity-v(\d+)-retired$/u;
|
|
510
|
+
|
|
511
|
+
function retiredIntegritySourceId(runId: string, receiptVersion: number): string {
|
|
512
|
+
return `run_receipt:${runId}:integrity-v${receiptVersion}-retired`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* The retired integrity version a projected artifact-integrity axis names, or
|
|
517
|
+
* null when the axis says something else. The compatibility source id is the
|
|
518
|
+
* only place the persisted projection carries the version, so the human clause
|
|
519
|
+
* reads it back from there rather than from the receipt.
|
|
520
|
+
*/
|
|
521
|
+
export function retiredIntegrityVersionOf(status: ArtifactIntegrityStatus): number | null {
|
|
522
|
+
if (status.state !== "unknown" || status.source.kind !== "compatibility") return null;
|
|
523
|
+
const match = RETIRED_INTEGRITY_SOURCE.exec(status.source.id);
|
|
524
|
+
return match === null ? null : Number(match[1]);
|
|
525
|
+
}
|
|
526
|
+
|
|
462
527
|
/** Adapt receipt integrity verification without reading any receipt claim as self-authenticating. */
|
|
463
528
|
export function adaptReceiptIntegrityStatus(
|
|
464
529
|
receipt: PersistedRunReceiptTrustFacts | null | undefined,
|
|
465
|
-
verification?:
|
|
530
|
+
verification?: ReceiptIntegrityOutcome,
|
|
466
531
|
): ArtifactIntegrityStatus {
|
|
467
532
|
if (receipt === null || receipt === undefined) return absentTrustStatus("artifact_missing");
|
|
468
533
|
const artifacts = [receiptReference(receipt)];
|
|
534
|
+
// A retired seal was never checked, so the axis is unknown rather than
|
|
535
|
+
// failed, and the compatibility source names the version it was sealed
|
|
536
|
+
// under. `failed` stays reserved for a seal this build verified and rejected.
|
|
537
|
+
if (verification !== undefined && !verification.ok && verification.retired !== undefined) {
|
|
538
|
+
return attributed(
|
|
539
|
+
"unknown",
|
|
540
|
+
{ kind: "compatibility", id: retiredIntegritySourceId(receipt.runId, verification.retired.receiptVersion) },
|
|
541
|
+
COMPATIBILITY_AUTHORITY,
|
|
542
|
+
artifacts,
|
|
543
|
+
);
|
|
544
|
+
}
|
|
469
545
|
if (verification === undefined || (verification.ok && receipt.integrity === undefined)) {
|
|
470
546
|
return attributed(
|
|
471
547
|
"unknown",
|
|
@@ -576,15 +652,46 @@ function validBoundedProjectContext(context: RunProjectContextProvenance): boole
|
|
|
576
652
|
);
|
|
577
653
|
}
|
|
578
654
|
|
|
655
|
+
/**
|
|
656
|
+
* The one structured message a `none`-tier run can still receive. The tier is
|
|
657
|
+
* the CLIO-CODER.md handbook policy; the workspace-root message is sent
|
|
658
|
+
* regardless of it, and the producer records the characters and hash of
|
|
659
|
+
* every structured message it sent (`projectContextProvenanceFor` in the
|
|
660
|
+
* dispatch extension). A none-tier block naming exactly this section, with
|
|
661
|
+
* a well-formed count and hash, is therefore a consistent record of a real
|
|
662
|
+
* message, not a contradiction. Anything else under a none tier cannot be
|
|
663
|
+
* explained by the policy: a handbook section the policy forbids, a hash
|
|
664
|
+
* with no section to hash, or a malformed count.
|
|
665
|
+
*/
|
|
666
|
+
const NONE_TIER_SECTIONS: ReadonlyArray<string> = ["workspace-root"];
|
|
667
|
+
|
|
668
|
+
function validNoneTierProjectContext(context: RunProjectContextProvenance): boolean {
|
|
669
|
+
return (
|
|
670
|
+
context.tier === "none" &&
|
|
671
|
+
context.sections !== undefined &&
|
|
672
|
+
context.sections.length === NONE_TIER_SECTIONS.length &&
|
|
673
|
+
context.sections.every((section) => NONE_TIER_SECTIONS.includes(section)) &&
|
|
674
|
+
Number.isSafeInteger(context.chars) &&
|
|
675
|
+
(context.chars ?? -1) >= 0 &&
|
|
676
|
+
typeof context.contentHash === "string" &&
|
|
677
|
+
/^[a-f0-9]{64}$/u.test(context.contentHash)
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function recordedProjectContextHash(context: RunProjectContextProvenance | undefined): string | undefined {
|
|
682
|
+
if (context === undefined) return undefined;
|
|
683
|
+
if (context.tier === "bounded") return validBoundedProjectContext(context) ? context.contentHash : undefined;
|
|
684
|
+
return validNoneTierProjectContext(context) ? context.contentHash : undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
579
687
|
function contextArtifacts(receipt: PersistedRunReceiptTrustFacts): TrustArtifactReference[] {
|
|
580
688
|
const artifacts = [receiptReference(receipt)];
|
|
581
689
|
if (receipt.briefing !== undefined && validBriefing(receipt.briefing)) {
|
|
582
690
|
artifacts.push(sha256Reference("briefing", `${receipt.runId}:briefing`, receipt.briefing.contentHash));
|
|
583
691
|
}
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
);
|
|
692
|
+
const recordedHash = recordedProjectContextHash(receipt.projectContext);
|
|
693
|
+
if (recordedHash !== undefined) {
|
|
694
|
+
artifacts.push(sha256Reference("project_context", `${receipt.runId}:project-context`, recordedHash));
|
|
588
695
|
}
|
|
589
696
|
return uniqueBoundedReferences(artifacts);
|
|
590
697
|
}
|
|
@@ -604,12 +711,23 @@ export function adaptRunReceiptContextStatus(
|
|
|
604
711
|
return attributed("unknown", compatibilitySource(receipt, "projectContext"), COMPATIBILITY_AUTHORITY, artifacts);
|
|
605
712
|
}
|
|
606
713
|
if (context.tier === "none") {
|
|
607
|
-
|
|
608
|
-
|
|
714
|
+
const bare = context.chars === undefined && context.contentHash === undefined && context.sections === undefined;
|
|
715
|
+
if (bare) {
|
|
716
|
+
// Nothing structured was sent: the axis does not apply unless a
|
|
717
|
+
// briefing was recorded, which is context of its own.
|
|
718
|
+
return receipt.briefing === undefined
|
|
719
|
+
? attributed("not_applicable", source, DISPATCH_AUTHORITY, artifacts)
|
|
720
|
+
: attributed("recorded", source, DISPATCH_AUTHORITY, artifacts);
|
|
609
721
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
722
|
+
// The workspace-root message is the one thing a none-tier run receives,
|
|
723
|
+
// and its record is valid; every other none-tier content shape is a
|
|
724
|
+
// record the policy cannot account for.
|
|
725
|
+
return attributed(
|
|
726
|
+
validNoneTierProjectContext(context) ? "recorded" : "invalid",
|
|
727
|
+
source,
|
|
728
|
+
DISPATCH_AUTHORITY,
|
|
729
|
+
artifacts,
|
|
730
|
+
);
|
|
613
731
|
}
|
|
614
732
|
return attributed(validBoundedProjectContext(context) ? "recorded" : "invalid", source, DISPATCH_AUTHORITY, artifacts);
|
|
615
733
|
}
|
|
@@ -639,18 +757,19 @@ export function adaptRunReceiptAutonomyStatus(
|
|
|
639
757
|
}
|
|
640
758
|
|
|
641
759
|
export interface AdaptRunReceiptTrustOptions {
|
|
642
|
-
integrity?:
|
|
760
|
+
integrity?: ReceiptIntegrityOutcome;
|
|
643
761
|
}
|
|
644
762
|
|
|
645
763
|
function receiptClaimsAreAuthenticated(
|
|
646
764
|
receipt: PersistedRunReceiptTrustFacts | null | undefined,
|
|
647
|
-
integrity:
|
|
765
|
+
integrity: ReceiptIntegrityOutcome | undefined,
|
|
648
766
|
): boolean {
|
|
649
767
|
return receipt?.integrity !== undefined && integrity?.ok === true;
|
|
650
768
|
}
|
|
651
769
|
|
|
652
770
|
function unauthenticatedReceiptProjection(
|
|
653
771
|
receipt: PersistedRunReceiptTrustFacts | null | undefined,
|
|
772
|
+
integrity: ReceiptIntegrityOutcome | undefined,
|
|
654
773
|
): TrustStatusProjection {
|
|
655
774
|
if (receipt === null || receipt === undefined) {
|
|
656
775
|
return {
|
|
@@ -682,6 +801,15 @@ function unauthenticatedReceiptProjection(
|
|
|
682
801
|
),
|
|
683
802
|
};
|
|
684
803
|
}
|
|
804
|
+
// A retired seal leaves the receipt unread rather than rejected: its axes
|
|
805
|
+
// are absent because the format is historical, not because a check failed.
|
|
806
|
+
if (integrity !== undefined && !integrity.ok && integrity.retired !== undefined) {
|
|
807
|
+
return {
|
|
808
|
+
validationGrounding: absentTrustStatus("historical_format"),
|
|
809
|
+
contextProvenance: absentTrustStatus("historical_format"),
|
|
810
|
+
autonomyEnforcement: absentTrustStatus("historical_format"),
|
|
811
|
+
};
|
|
812
|
+
}
|
|
685
813
|
// The artifact-integrity axis retains the failure diagnostic. No other
|
|
686
814
|
// axis reads a field from a receipt that failed authentication.
|
|
687
815
|
return {
|
|
@@ -703,7 +831,7 @@ export function adaptRunReceiptTrustStatus(
|
|
|
703
831
|
contextProvenance: adaptRunReceiptContextStatus(receipt),
|
|
704
832
|
autonomyEnforcement: adaptRunReceiptAutonomyStatus(receipt),
|
|
705
833
|
}
|
|
706
|
-
: unauthenticatedReceiptProjection(receipt);
|
|
834
|
+
: unauthenticatedReceiptProjection(receipt, options.integrity);
|
|
707
835
|
return composeTrustStatus({
|
|
708
836
|
artifactIntegrity: adaptReceiptIntegrityStatus(receipt, options.integrity),
|
|
709
837
|
...receiptProjection,
|
|
@@ -715,7 +843,7 @@ export function adaptRunReceiptTrustStatus(
|
|
|
715
843
|
}
|
|
716
844
|
|
|
717
845
|
export interface RunReceiptTrustInspection {
|
|
718
|
-
integrity:
|
|
846
|
+
integrity: ReceiptIntegrityOutcome;
|
|
719
847
|
status: CanonicalTrustStatus;
|
|
720
848
|
}
|
|
721
849
|
|
|
@@ -728,14 +856,14 @@ export function inspectRunReceiptTrustStatus(
|
|
|
728
856
|
receipt: RunReceipt | null | undefined,
|
|
729
857
|
envelope: RunEnvelope | null | undefined,
|
|
730
858
|
): RunReceiptTrustInspection {
|
|
731
|
-
let integrity:
|
|
859
|
+
let integrity: ReceiptIntegrityOutcome;
|
|
732
860
|
if (receipt === null || receipt === undefined) {
|
|
733
861
|
integrity = { ok: false, reason: "receipt unavailable" };
|
|
734
862
|
} else if (envelope === null || envelope === undefined) {
|
|
735
863
|
integrity = { ok: false, reason: "run ledger envelope unavailable" };
|
|
736
864
|
} else {
|
|
737
865
|
try {
|
|
738
|
-
integrity =
|
|
866
|
+
integrity = verifyReceiptIntegrityOutcome(receipt, envelope);
|
|
739
867
|
} catch (error) {
|
|
740
868
|
integrity = {
|
|
741
869
|
ok: false,
|
|
@@ -23,7 +23,11 @@ export const EVIDENCE_TAGS = [
|
|
|
23
23
|
"escalation",
|
|
24
24
|
"external-bypass",
|
|
25
25
|
"external-approximation",
|
|
26
|
+
"independent-review",
|
|
27
|
+
"context-provenance",
|
|
28
|
+
"completion-evidence",
|
|
26
29
|
"receipt-integrity",
|
|
30
|
+
"receipt-retired",
|
|
27
31
|
"protected-artifact",
|
|
28
32
|
"tool-loop",
|
|
29
33
|
"test-failure",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
4
4
|
import type {
|
|
@@ -39,13 +39,99 @@ function normalizeResources(value: unknown): ExtensionManifestResources {
|
|
|
39
39
|
const out: ExtensionManifestResources = {};
|
|
40
40
|
const skills = trimString(value.skills);
|
|
41
41
|
const prompts = trimString(value.prompts);
|
|
42
|
+
const agents = trimString(value.agents);
|
|
43
|
+
const fleets = trimString(value.fleets);
|
|
42
44
|
const themes = trimString(value.themes);
|
|
43
45
|
if (skills) out.skills = skills;
|
|
44
46
|
if (prompts) out.prompts = prompts;
|
|
47
|
+
if (agents) out.agents = agents;
|
|
48
|
+
if (fleets) out.fleets = fleets;
|
|
45
49
|
if (themes) out.themes = themes;
|
|
46
50
|
return out;
|
|
47
51
|
}
|
|
48
52
|
|
|
53
|
+
function contained(root: string, candidate: string): boolean {
|
|
54
|
+
const relative = path.relative(root, candidate);
|
|
55
|
+
return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateResourceTree(
|
|
59
|
+
root: string,
|
|
60
|
+
resourcePath: string,
|
|
61
|
+
kind: string,
|
|
62
|
+
diagnostics: ExtensionDiagnostic[],
|
|
63
|
+
): void {
|
|
64
|
+
if (path.isAbsolute(resourcePath)) {
|
|
65
|
+
diagnostics.push({ type: "error", message: `resources.${kind} must be relative`, path: resourcePath });
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const resolvedRoot = path.resolve(root);
|
|
69
|
+
const resolved = path.resolve(resolvedRoot, resourcePath);
|
|
70
|
+
if (!contained(resolvedRoot, resolved) || resolved === resolvedRoot) {
|
|
71
|
+
diagnostics.push({ type: "error", message: `resources.${kind} escapes the extension root`, path: resourcePath });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
let canonicalRoot: string;
|
|
75
|
+
let canonicalResource: string;
|
|
76
|
+
try {
|
|
77
|
+
canonicalRoot = realpathSync(resolvedRoot);
|
|
78
|
+
canonicalResource = realpathSync(resolved);
|
|
79
|
+
if (!lstatSync(resolved).isDirectory()) {
|
|
80
|
+
diagnostics.push({ type: "error", message: `resources.${kind} is not a directory`, path: resolved });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
} catch (error) {
|
|
84
|
+
diagnostics.push({
|
|
85
|
+
type: "error",
|
|
86
|
+
message: `resources.${kind} cannot be resolved: ${error instanceof Error ? error.message : String(error)}`,
|
|
87
|
+
path: resolved,
|
|
88
|
+
});
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!contained(canonicalRoot, canonicalResource)) {
|
|
92
|
+
diagnostics.push({ type: "error", message: `resources.${kind} escapes through a symbolic link`, path: resolved });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const pending = [resolved];
|
|
97
|
+
while (pending.length > 0) {
|
|
98
|
+
const directory = pending.pop();
|
|
99
|
+
if (!directory) continue;
|
|
100
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
101
|
+
const candidate = path.join(directory, entry.name);
|
|
102
|
+
if (entry.isSymbolicLink()) {
|
|
103
|
+
try {
|
|
104
|
+
if (!contained(canonicalRoot, realpathSync(candidate))) {
|
|
105
|
+
diagnostics.push({
|
|
106
|
+
type: "error",
|
|
107
|
+
message: `resources.${kind} contains an escaping symbolic link`,
|
|
108
|
+
path: candidate,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
diagnostics.push({
|
|
113
|
+
type: "error",
|
|
114
|
+
message: `resources.${kind} contains an unresolved symbolic link: ${error instanceof Error ? error.message : String(error)}`,
|
|
115
|
+
path: candidate,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
} else if (entry.isDirectory()) {
|
|
119
|
+
pending.push(candidate);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function validateResourceRoots(
|
|
126
|
+
root: string,
|
|
127
|
+
resources: ExtensionManifestResources,
|
|
128
|
+
diagnostics: ExtensionDiagnostic[],
|
|
129
|
+
): void {
|
|
130
|
+
for (const [kind, resourcePath] of Object.entries(resources)) {
|
|
131
|
+
if (resourcePath) validateResourceTree(root, resourcePath, kind, diagnostics);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
49
135
|
function stringArray(value: unknown): string[] | undefined {
|
|
50
136
|
if (!Array.isArray(value)) return undefined;
|
|
51
137
|
const out = value.map((entry) => trimString(entry)).filter((entry): entry is string => entry !== undefined);
|
|
@@ -123,6 +209,7 @@ export function loadManifestFromRoot(root: string): ExtensionCandidate {
|
|
|
123
209
|
}
|
|
124
210
|
try {
|
|
125
211
|
const parsed = parseExtensionManifest(readJsonOrYaml(manifestPath), manifestPath);
|
|
212
|
+
if (parsed.manifest) validateResourceRoots(root, parsed.manifest.resources, parsed.diagnostics);
|
|
126
213
|
return {
|
|
127
214
|
path: root,
|
|
128
215
|
manifestPath,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { listInstalledExtensions } from "./state.js";
|
|
4
4
|
import type { ExtensionResourceKind, ExtensionResourceRoot } from "./types.js";
|
|
@@ -7,8 +7,24 @@ export function extensionResourcePath(rootPath: string, resourcePath: string): s
|
|
|
7
7
|
const root = path.resolve(rootPath);
|
|
8
8
|
const full = path.resolve(root, resourcePath);
|
|
9
9
|
const relative = path.relative(root, full);
|
|
10
|
-
if (relative === "" ||
|
|
11
|
-
|
|
10
|
+
if (relative === "" || relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative))
|
|
11
|
+
return null;
|
|
12
|
+
try {
|
|
13
|
+
const rootReal = realpathSync(root);
|
|
14
|
+
const fullReal = realpathSync(full);
|
|
15
|
+
const canonicalRelative = path.relative(rootReal, fullReal);
|
|
16
|
+
if (
|
|
17
|
+
canonicalRelative === ".." ||
|
|
18
|
+
canonicalRelative.startsWith(`..${path.sep}`) ||
|
|
19
|
+
path.isAbsolute(canonicalRelative)
|
|
20
|
+
) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
if (!lstatSync(full).isDirectory()) return null;
|
|
24
|
+
return full;
|
|
25
|
+
} catch {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
12
28
|
}
|
|
13
29
|
|
|
14
30
|
export function enabledExtensionResourceRoots(
|
|
@@ -22,15 +38,11 @@ export function enabledExtensionResourceRoots(
|
|
|
22
38
|
if (!rel) continue;
|
|
23
39
|
const full = extensionResourcePath(entry.rootPath, rel);
|
|
24
40
|
if (!full) continue;
|
|
25
|
-
try {
|
|
26
|
-
if (!statSync(full).isDirectory()) continue;
|
|
27
|
-
} catch {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
41
|
roots.push({
|
|
31
42
|
id: entry.id,
|
|
32
43
|
scope: entry.scope,
|
|
33
44
|
path: full,
|
|
45
|
+
rootPath: entry.rootPath,
|
|
34
46
|
source: `extension:${entry.scope}:${entry.id}`,
|
|
35
47
|
});
|
|
36
48
|
}
|
|
@@ -129,7 +129,7 @@ export function installExtension(sourcePath: string, options: ExtensionInstallOp
|
|
|
129
129
|
const cwd = options.cwd ?? process.cwd();
|
|
130
130
|
const source = path.resolve(sourcePath);
|
|
131
131
|
const candidate = loadManifestFromRoot(source);
|
|
132
|
-
if (!candidate.manifest) return { diagnostics: candidate.diagnostics };
|
|
132
|
+
if (!candidate.manifest || !candidate.valid) return { diagnostics: candidate.diagnostics };
|
|
133
133
|
const targetRoot = path.join(extensionBaseDir(scope, cwd), candidate.manifest.id);
|
|
134
134
|
if (existsSync(targetRoot)) {
|
|
135
135
|
if (!options.force) {
|
|
@@ -152,7 +152,11 @@ export function installExtension(sourcePath: string, options: ExtensionInstallOp
|
|
|
152
152
|
rmSync(backupRoot, { recursive: true, force: true });
|
|
153
153
|
cpSync(source, stagingRoot, {
|
|
154
154
|
recursive: true,
|
|
155
|
-
|
|
155
|
+
// `state.json` beside the installed extension directories belongs to
|
|
156
|
+
// Clio's extension manager. An extension may legitimately ship nested
|
|
157
|
+
// resources with the same basename (for example a project-state JSON
|
|
158
|
+
// template), so exclude only a bookkeeping file at the package root.
|
|
159
|
+
filter: (src) => path.relative(source, src) !== "state.json",
|
|
156
160
|
});
|
|
157
161
|
if (existsSync(targetRoot)) {
|
|
158
162
|
renameSync(targetRoot, backupRoot);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
export type ExtensionScope = "user" | "project";
|
|
2
|
-
export type ExtensionResourceKind = "skills" | "prompts" | "themes";
|
|
2
|
+
export type ExtensionResourceKind = "skills" | "prompts" | "agents" | "fleets" | "themes";
|
|
3
3
|
|
|
4
4
|
export interface ExtensionManifestResources {
|
|
5
5
|
skills?: string;
|
|
6
6
|
prompts?: string;
|
|
7
|
+
agents?: string;
|
|
8
|
+
fleets?: string;
|
|
7
9
|
themes?: string;
|
|
8
10
|
}
|
|
9
11
|
|
|
@@ -52,6 +54,7 @@ export interface ExtensionResourceRoot {
|
|
|
52
54
|
id: string;
|
|
53
55
|
scope: ExtensionScope;
|
|
54
56
|
path: string;
|
|
57
|
+
rootPath: string;
|
|
55
58
|
source: string;
|
|
56
59
|
}
|
|
57
60
|
|
|
@@ -5,8 +5,11 @@ import { initializeClioHome } from "../../core/init.js";
|
|
|
5
5
|
import { resolveClioDirs } from "../../core/xdg.js";
|
|
6
6
|
import { readSessionFileEntries, type SessionJsonlWarning } from "../../engine/session.js";
|
|
7
7
|
import { detectInteropAgents, interopAgentKind, resolveOnPath } from "../interop/index.js";
|
|
8
|
-
import { openAuthStorage } from "../providers/auth/index.js";
|
|
8
|
+
import { openAuthStorage, resolveAuthTarget, targetRequiresAuth } from "../providers/auth/index.js";
|
|
9
|
+
import { credentialsPresent } from "../providers/credentials.js";
|
|
9
10
|
import { fingerprintNativeRuntime } from "../providers/probe/fingerprint.js";
|
|
11
|
+
import type { ProbeContext, ProbeResult, RuntimeDescriptor } from "../providers/types/runtime-descriptor.js";
|
|
12
|
+
import type { TargetDescriptor } from "../providers/types/target-descriptor.js";
|
|
10
13
|
import { loadSkills, type SkillSource } from "../resources/skills/loader.js";
|
|
11
14
|
import { readStateInfoResult } from "./state.js";
|
|
12
15
|
import { getVersionInfo } from "./version.js";
|
|
@@ -534,3 +537,139 @@ export async function runDoctorRuntimeChecks(): Promise<DoctorFinding[]> {
|
|
|
534
537
|
);
|
|
535
538
|
return results.filter((finding): finding is DoctorFinding => finding !== null);
|
|
536
539
|
}
|
|
540
|
+
|
|
541
|
+
/** One configured model pointer at a target, named the way settings.yaml spells it. */
|
|
542
|
+
interface ConfiguredModelRole {
|
|
543
|
+
role: string;
|
|
544
|
+
model: string;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function configuredModelRoles(
|
|
548
|
+
settings: ReturnType<typeof readSettings>,
|
|
549
|
+
target: TargetDescriptor,
|
|
550
|
+
): ConfiguredModelRole[] {
|
|
551
|
+
const roles: ConfiguredModelRole[] = [];
|
|
552
|
+
if (target.defaultModel) roles.push({ role: "defaultModel", model: target.defaultModel });
|
|
553
|
+
if (settings.orchestrator.target === target.id && settings.orchestrator.model) {
|
|
554
|
+
roles.push({ role: "orchestrator.model", model: settings.orchestrator.model });
|
|
555
|
+
}
|
|
556
|
+
if (settings.background.target === target.id && settings.background.model) {
|
|
557
|
+
roles.push({ role: "background.model", model: settings.background.model });
|
|
558
|
+
}
|
|
559
|
+
if (settings.workers.default.target === target.id && settings.workers.default.model) {
|
|
560
|
+
roles.push({ role: "workers.default.model", model: settings.workers.default.model });
|
|
561
|
+
}
|
|
562
|
+
return roles;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Per-request budget for the model sweep. A target that does not answer in
|
|
567
|
+
* this time falls back to the list configure recorded, so a black-holed
|
|
568
|
+
* remote costs doctor a bounded wait rather than the probe's full timeout.
|
|
569
|
+
*/
|
|
570
|
+
const DOCTOR_MODEL_PROBE_TIMEOUT_MS = 2_500;
|
|
571
|
+
|
|
572
|
+
async function doctorProbeContext(target: TargetDescriptor, runtime: RuntimeDescriptor): Promise<ProbeContext> {
|
|
573
|
+
const ctx: ProbeContext = { credentialsPresent: credentialsPresent(), httpTimeoutMs: DOCTOR_MODEL_PROBE_TIMEOUT_MS };
|
|
574
|
+
if (!targetRequiresAuth(target, runtime)) return ctx;
|
|
575
|
+
try {
|
|
576
|
+
const resolution = await openAuthStorage().resolveForTarget(resolveAuthTarget(target, runtime), {
|
|
577
|
+
includeFallback: false,
|
|
578
|
+
});
|
|
579
|
+
if (resolution.apiKey) ctx.authToken = resolution.apiKey;
|
|
580
|
+
} catch {
|
|
581
|
+
// The probe reports its own missing-auth failure; the wireModels fallback covers the check.
|
|
582
|
+
}
|
|
583
|
+
return ctx;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/** What the target advertises now, or null when it could not be asked. */
|
|
587
|
+
async function probeAdvertisedModels(
|
|
588
|
+
target: TargetDescriptor,
|
|
589
|
+
runtime: RuntimeDescriptor,
|
|
590
|
+
): Promise<{ advertised: string[]; resident: string[] } | null> {
|
|
591
|
+
if (runtime.kind !== "http" || typeof runtime.probe !== "function" || !target.url) return null;
|
|
592
|
+
let probe: ProbeResult;
|
|
593
|
+
try {
|
|
594
|
+
probe = await runtime.probe(target, await doctorProbeContext(target, runtime));
|
|
595
|
+
} catch {
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
if (!probe.ok || !probe.models || probe.models.length === 0) return null;
|
|
599
|
+
const advertised = [...probe.models];
|
|
600
|
+
const resident: string[] = [];
|
|
601
|
+
for (const [id, status] of Object.entries(probe.modelStates ?? {})) {
|
|
602
|
+
if (!advertised.includes(id)) advertised.push(id);
|
|
603
|
+
if (status.state === "loaded" || status.state === "loading") resident.push(id);
|
|
604
|
+
}
|
|
605
|
+
return { advertised, resident };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Model sweep: every model pointer settings.yaml aims at a target with no
|
|
610
|
+
* static catalog is checked against what that target advertises. The live
|
|
611
|
+
* list wins when the target answers; the `wireModels` list configure recorded
|
|
612
|
+
* stands in when it does not, so an unreachable server still gets the
|
|
613
|
+
* placeholder id it was saved with called out. A target with neither list is
|
|
614
|
+
* a WARN, not a pass, because nothing was verified. Network-bound like the
|
|
615
|
+
* runtime sweep, so it is not part of the synchronous `runDoctor()` core.
|
|
616
|
+
*/
|
|
617
|
+
export async function runDoctorModelChecks(): Promise<DoctorFinding[]> {
|
|
618
|
+
let settings: ReturnType<typeof readSettings>;
|
|
619
|
+
try {
|
|
620
|
+
settings = readSettings();
|
|
621
|
+
} catch {
|
|
622
|
+
return [];
|
|
623
|
+
}
|
|
624
|
+
if (settings.targets.length === 0) return [];
|
|
625
|
+
// Every runtime descriptor and the model catalog sit behind these two
|
|
626
|
+
// imports. They are loaded here, not at module load, so a doctor run on a
|
|
627
|
+
// home with no targets (the `--fix` seed every CLI test starts from) pays
|
|
628
|
+
// nothing for a sweep it has nothing to do.
|
|
629
|
+
const [{ getRuntimeRegistry }, { registerBuiltinRuntimes }, { listKnownModelsForRuntime }] = await Promise.all([
|
|
630
|
+
import("../providers/registry.js"),
|
|
631
|
+
import("../providers/runtimes/builtins.js"),
|
|
632
|
+
import("../providers/support.js"),
|
|
633
|
+
]);
|
|
634
|
+
const registry = getRuntimeRegistry();
|
|
635
|
+
// doctor never loads the providers domain, so the registry is empty here
|
|
636
|
+
// unless another command in this process filled it.
|
|
637
|
+
if (registry.list().length === 0) registerBuiltinRuntimes(registry);
|
|
638
|
+
const results = await Promise.all(
|
|
639
|
+
settings.targets.map(async (target): Promise<DoctorFinding | null> => {
|
|
640
|
+
const runtime = registry.get(target.runtime);
|
|
641
|
+
if (!runtime) return null;
|
|
642
|
+
// Cloud runtimes are validated against their catalog at configure time.
|
|
643
|
+
if (listKnownModelsForRuntime(runtime.id).length > 0) return null;
|
|
644
|
+
const roles = configuredModelRoles(settings, target);
|
|
645
|
+
if (roles.length === 0) return null;
|
|
646
|
+
const live = await probeAdvertisedModels(target, runtime);
|
|
647
|
+
const recorded = target.wireModels ?? [];
|
|
648
|
+
if (live === null && recorded.length === 0) {
|
|
649
|
+
return {
|
|
650
|
+
ok: true,
|
|
651
|
+
level: "warn",
|
|
652
|
+
name: `model ${target.id}`,
|
|
653
|
+
detail: `${roles.map((entry) => `${entry.role} '${entry.model}'`).join(", ")} could not be verified: the target did not answer and configure recorded no model list; run \`clio-coder targets --probe\` once it is up`,
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
const advertised = live ? live.advertised : recorded;
|
|
657
|
+
const source = live ? `advertised by ${target.url ?? target.id} now` : "recorded by configure at last save";
|
|
658
|
+
const missing = roles.filter((entry) => !advertised.includes(entry.model));
|
|
659
|
+
if (missing.length === 0) {
|
|
660
|
+
return {
|
|
661
|
+
ok: true,
|
|
662
|
+
name: `model ${target.id}`,
|
|
663
|
+
detail: `${roles.map((entry) => `${entry.role} '${entry.model}'`).join(", ")} ${source}`,
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const resident = live && live.resident.length > 0 ? live.resident.join(", ") : live ? "none" : "unknown";
|
|
667
|
+
return {
|
|
668
|
+
ok: false,
|
|
669
|
+
name: `model ${target.id}`,
|
|
670
|
+
detail: `${missing.map((entry) => `${entry.role} '${entry.model}'`).join(", ")} not ${source} (${advertised.length} ids). Resident instances: ${resident}. Re-run \`clio-coder configure --id ${target.id} --model <advertised id>\`; \`clio-coder targets --probe\` lists them`,
|
|
671
|
+
};
|
|
672
|
+
}),
|
|
673
|
+
);
|
|
674
|
+
return results.filter((finding): finding is DoctorFinding => finding !== null);
|
|
675
|
+
}
|
|
@@ -15,11 +15,9 @@ export interface CompileWorkerPromptInput extends WorkerPromptInputs {
|
|
|
15
15
|
/** Working directory the rule loader and repo-awareness checks run against; defaults to `process.cwd()`. */
|
|
16
16
|
cwd?: string;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* is no structured path field on the model-facing dispatch tool, so this is
|
|
22
|
-
* inference, not a guarantee: a rule can be missed, never fabricated.
|
|
18
|
+
* Canonical paths for this worker's declared intent. Legacy requests without
|
|
19
|
+
* intent retain path-like task and briefing inference. The prompt compiler
|
|
20
|
+
* consumes the resolved set without deriving another one.
|
|
23
21
|
*/
|
|
24
22
|
workingContextPaths?: ReadonlyArray<string>;
|
|
25
23
|
}
|