@ashlr/hub 3.2.7 → 3.3.2
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 +314 -2
- package/README.md +54 -38
- package/dist/build-identity.json +2 -2
- package/dist/cli/audit.js +18 -0
- package/dist/cli/audit.js.map +1 -1
- package/dist/cli/backlog.d.ts +0 -5
- package/dist/cli/backlog.js +18 -0
- package/dist/cli/backlog.js.map +1 -1
- package/dist/cli/comms.js +20 -0
- package/dist/cli/comms.js.map +1 -1
- package/dist/cli/completions.d.ts +6 -1
- package/dist/cli/completions.js +33 -10
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/daemon.js +10 -3
- package/dist/cli/daemon.js.map +1 -1
- package/dist/cli/dashboard.js +5 -4
- package/dist/cli/dashboard.js.map +1 -1
- package/dist/cli/demo.d.ts +0 -27
- package/dist/cli/demo.js +17 -0
- package/dist/cli/demo.js.map +1 -1
- package/dist/cli/help.js +22 -9
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/inbox.d.ts +14 -2
- package/dist/cli/inbox.js +137 -23
- package/dist/cli/inbox.js.map +1 -1
- package/dist/cli/onboard.d.ts +0 -13
- package/dist/cli/onboard.js +20 -0
- package/dist/cli/onboard.js.map +1 -1
- package/dist/cli/resources.js +15 -0
- package/dist/cli/resources.js.map +1 -1
- package/dist/cli/run.js +1 -1
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/sandbox.js +43 -0
- package/dist/cli/sandbox.js.map +1 -1
- package/dist/cli/serve.js +7 -3
- package/dist/cli/serve.js.map +1 -1
- package/dist/cli/setup.d.ts +5 -3
- package/dist/cli/setup.js +14 -7
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/tui.d.ts +0 -6
- package/dist/cli/tui.js +14 -0
- package/dist/cli/tui.js.map +1 -1
- package/dist/cli/update.js +3 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/core/config.js +70 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/daemon/loop.js +83 -3
- package/dist/core/daemon/loop.js.map +1 -1
- package/dist/core/daemon/runtime-activation-resident-start-protocol.d.ts +141 -0
- package/dist/core/daemon/runtime-activation-resident-start-protocol.js +462 -0
- package/dist/core/daemon/runtime-activation-resident-start-protocol.js.map +1 -0
- package/dist/core/daemon/runtime-activation-resident-start-runtime.d.ts +27 -0
- package/dist/core/daemon/runtime-activation-resident-start-runtime.js +28 -0
- package/dist/core/daemon/runtime-activation-resident-start-runtime.js.map +1 -0
- package/dist/core/daemon/runtime-activation-stopped-consumer.d.ts +139 -0
- package/dist/core/daemon/runtime-activation-stopped-consumer.js +995 -0
- package/dist/core/daemon/runtime-activation-stopped-consumer.js.map +1 -0
- package/dist/core/daemon/runtime-activation-stopped-runtime.d.ts +36 -0
- package/dist/core/daemon/runtime-activation-stopped-runtime.js +134 -0
- package/dist/core/daemon/runtime-activation-stopped-runtime.js.map +1 -0
- package/dist/core/daemon/runtime-activation-transaction.d.ts +41 -17
- package/dist/core/daemon/runtime-activation-transaction.js +80 -11
- package/dist/core/daemon/runtime-activation-transaction.js.map +1 -1
- package/dist/core/daemon/runtime-release-dependency-inventory.js +1 -0
- package/dist/core/daemon/runtime-release-dependency-inventory.js.map +1 -1
- package/dist/core/daemon/service-install-authority.d.ts +1 -1
- package/dist/core/daemon/service-install-authority.js +4 -2
- package/dist/core/daemon/service-install-authority.js.map +1 -1
- package/dist/core/dashboard.d.ts +10 -0
- package/dist/core/dashboard.js +103 -28
- package/dist/core/dashboard.js.map +1 -1
- package/dist/core/effective-config.js +7 -0
- package/dist/core/effective-config.js.map +1 -1
- package/dist/core/fleet/automerge-pass.js +57 -73
- package/dist/core/fleet/automerge-pass.js.map +1 -1
- package/dist/core/fleet/decisions-ledger.js +36 -13
- package/dist/core/fleet/decisions-ledger.js.map +1 -1
- package/dist/core/fleet/external-skill-git-capture.js +9 -1
- package/dist/core/fleet/external-skill-git-capture.js.map +1 -1
- package/dist/core/fleet/judge-decision-metadata.d.ts +8 -1
- package/dist/core/fleet/judge-decision-metadata.js +14 -1
- package/dist/core/fleet/judge-decision-metadata.js.map +1 -1
- package/dist/core/fleet/judge-trace.js +2 -1
- package/dist/core/fleet/judge-trace.js.map +1 -1
- package/dist/core/fleet/manager.d.ts +21 -1
- package/dist/core/fleet/manager.js +137 -41
- package/dist/core/fleet/manager.js.map +1 -1
- package/dist/core/fleet/post-merge-credit-label.d.ts +10 -0
- package/dist/core/fleet/post-merge-credit-label.js +13 -0
- package/dist/core/fleet/post-merge-credit-label.js.map +1 -0
- package/dist/core/fleet/post-merge-credit.d.ts +119 -12
- package/dist/core/fleet/post-merge-credit.js +353 -14
- package/dist/core/fleet/post-merge-credit.js.map +1 -1
- package/dist/core/fleet/self-improve.d.ts +38 -5
- package/dist/core/fleet/self-improve.js +201 -12
- package/dist/core/fleet/self-improve.js.map +1 -1
- package/dist/core/fleet/skill-library.d.ts +21 -2
- package/dist/core/fleet/skill-library.js +54 -12
- package/dist/core/fleet/skill-library.js.map +1 -1
- package/dist/core/fleet/worked-ledger.d.ts +22 -0
- package/dist/core/fleet/worked-ledger.js +32 -0
- package/dist/core/fleet/worked-ledger.js.map +1 -1
- package/dist/core/goals/store.js +60 -6
- package/dist/core/goals/store.js.map +1 -1
- package/dist/core/inbox/apply.js +2 -1
- package/dist/core/inbox/apply.js.map +1 -1
- package/dist/core/inbox/merge.d.ts +10 -1
- package/dist/core/inbox/merge.js +97 -18
- package/dist/core/inbox/merge.js.map +1 -1
- package/dist/core/integrations/github.d.ts +2 -0
- package/dist/core/integrations/github.js +2 -1
- package/dist/core/integrations/github.js.map +1 -1
- package/dist/core/learn/reflect.d.ts +42 -4
- package/dist/core/learn/reflect.js +66 -5
- package/dist/core/learn/reflect.js.map +1 -1
- package/dist/core/onboard.d.ts +2 -0
- package/dist/core/onboard.js +9 -3
- package/dist/core/onboard.js.map +1 -1
- package/dist/core/run/engines.d.ts +8 -0
- package/dist/core/run/engines.js +9 -1
- package/dist/core/run/engines.js.map +1 -1
- package/dist/core/run/learned-router.js +28 -3
- package/dist/core/run/learned-router.js.map +1 -1
- package/dist/core/run/orchestrator.d.ts +7 -2
- package/dist/core/run/orchestrator.js +88 -60
- package/dist/core/run/orchestrator.js.map +1 -1
- package/dist/core/swarm/rollback.d.ts +11 -0
- package/dist/core/swarm/rollback.js +54 -0
- package/dist/core/swarm/rollback.js.map +1 -1
- package/dist/core/swarm/runner.js +94 -8
- package/dist/core/swarm/runner.js.map +1 -1
- package/dist/core/types.d.ts +103 -3
- package/dist/core/types.js.map +1 -1
- package/dist/core/web/api.d.ts +6 -0
- package/dist/core/web/api.js +174 -18
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/control.d.ts +20 -0
- package/dist/core/web/control.js +44 -32
- package/dist/core/web/control.js.map +1 -1
- package/dist/core/web/fleet-status-cache.d.ts +68 -0
- package/dist/core/web/fleet-status-cache.js +120 -0
- package/dist/core/web/fleet-status-cache.js.map +1 -0
- package/dist/core/web/public/app.js +5 -1
- package/dist/core/web/public/next/assets/index-CDHQraxS.css +1 -0
- package/dist/core/web/public/next/assets/index-zVKD9krw.js +69 -0
- package/dist/core/web/public/next/assets/index-zVKD9krw.js.map +1 -0
- package/dist/core/web/public/next/index.html +15 -0
- package/dist/core/web/read-session-revocations.d.ts +26 -0
- package/dist/core/web/read-session-revocations.js +48 -0
- package/dist/core/web/read-session-revocations.js.map +1 -0
- package/dist/core/web/server.js +73 -27
- package/dist/core/web/server.js.map +1 -1
- package/dist/core/web/static.d.ts +1 -1
- package/dist/core/web/static.js +54 -20
- package/dist/core/web/static.js.map +1 -1
- package/dist/release-dependency-inventory.json +1 -1
- package/docs/RUNTIME_ACTIVATION_AUTHORITY.md +115 -21
- package/docs/contracts/CONTRACT-M521.md +102 -0
- package/node_modules/fast-uri/index.js +201 -48
- package/node_modules/fast-uri/lib/schemes.js +9 -4
- package/node_modules/fast-uri/lib/utils.js +388 -91
- package/node_modules/fast-uri/package.json +1 -1
- package/node_modules/fast-uri/test/component-safe-serialization.test.js +105 -0
- package/node_modules/fast-uri/test/equal.test.js +31 -3
- package/node_modules/fast-uri/test/fixtures/uri-js-parse.json +2 -0
- package/node_modules/fast-uri/test/ipv6-canonical.test.js +34 -0
- package/node_modules/fast-uri/test/ipv6-validation.test.js +90 -0
- package/node_modules/fast-uri/test/malformed-percent.test.js +77 -0
- package/node_modules/fast-uri/test/malformed-urn.test.js +61 -0
- package/node_modules/fast-uri/test/parse.test.js +7 -3
- package/node_modules/fast-uri/test/query-fragment-normalization.test.js +33 -0
- package/node_modules/fast-uri/test/reserved-path-normalization.test.js +109 -0
- package/node_modules/fast-uri/test/scheme-validation.test.js +124 -0
- package/node_modules/fast-uri/test/security-normalization.test.js +101 -0
- package/node_modules/fast-uri/test/security.test.js +75 -3
- package/node_modules/fast-uri/test/urn-full-input.test.js +29 -0
- package/node_modules/fast-uri/test/websocket-query-preservation.test.js +24 -0
- package/node_modules/qs/.github/THREAT_MODEL.md +3 -3
- package/node_modules/qs/CHANGELOG.md +26 -0
- package/node_modules/qs/README.md +19 -1
- package/node_modules/qs/dist/qs.js +25 -25
- package/node_modules/qs/eslint.config.mjs +1 -1
- package/node_modules/qs/lib/parse.js +16 -6
- package/node_modules/qs/lib/stringify.js +23 -8
- package/node_modules/qs/lib/utils.js +57 -11
- package/node_modules/qs/package.json +8 -7
- package/node_modules/qs/test/parse.js +419 -0
- package/node_modules/qs/test/stringify.js +317 -0
- package/node_modules/qs/test/utils.js +206 -0
- package/package.json +24 -6
- package/schema/config.schema.json +800 -5
- package/node_modules/which/CHANGELOG.md +0 -166
package/dist/core/types.d.ts
CHANGED
|
@@ -730,6 +730,15 @@ export interface AshlrConfig {
|
|
|
730
730
|
kimi?: {
|
|
731
731
|
maxConcurrent?: number;
|
|
732
732
|
costPerMTokenOut?: number;
|
|
733
|
+
/**
|
|
734
|
+
* M270: high-level convenience override that promotes the 'kimi' engine
|
|
735
|
+
* spec's tier (engine-registry.ts:405 applyKimiConfig — e.g.
|
|
736
|
+
* `foundry.kimi = { tier: 'frontier' }` grants WORK-ASSIGNMENT frontier
|
|
737
|
+
* eligibility without touching the builtin registry). Previously read
|
|
738
|
+
* only via an untyped `(cfg?.foundry as Record<string, unknown>)?.['kimi']`
|
|
739
|
+
* cast; live in ~/.ashlr/config.json (tier: 'frontier') as of this audit.
|
|
740
|
+
*/
|
|
741
|
+
tier?: EngineTier;
|
|
733
742
|
};
|
|
734
743
|
/**
|
|
735
744
|
* M248: guarantee fleet sandboxed engines inherit ashlr-plugin MCP tools.
|
|
@@ -1074,12 +1083,12 @@ export interface AshlrConfig {
|
|
|
1074
1083
|
maxAutomergeFiles?: number;
|
|
1075
1084
|
/** Max changed lines permitted in an auto-merge diff (default 150, hard maximum 300). */
|
|
1076
1085
|
maxAutomergeLines?: number;
|
|
1077
|
-
/**
|
|
1086
|
+
/** Stage/push a reviewed branch and open a protected PR handoff; never merges the hosted PR (default false). */
|
|
1078
1087
|
pushToRemote?: boolean;
|
|
1079
1088
|
/**
|
|
1080
1089
|
* Remote-main policy expectation. This configuration is never
|
|
1081
1090
|
* authority by itself: live GitHub protection must match it immediately
|
|
1082
|
-
* before evidence capture, remote handoff, PR creation
|
|
1091
|
+
* before evidence capture, remote handoff, and PR creation.
|
|
1083
1092
|
*/
|
|
1084
1093
|
protectedRemote?: {
|
|
1085
1094
|
/** Operator expects live branch protection; autonomous remote-main delivery requires true. */
|
|
@@ -1310,6 +1319,47 @@ export interface AshlrConfig {
|
|
|
1310
1319
|
/** Ollama base URL (default 'http://localhost:11434'). */
|
|
1311
1320
|
baseUrl?: string;
|
|
1312
1321
|
};
|
|
1322
|
+
/**
|
|
1323
|
+
* M151: opt-in EDV (independent/external) verification signal folded into
|
|
1324
|
+
* backlog item scoring. DEFAULT false. Previously read only via an untyped
|
|
1325
|
+
* `(cfg.foundry as Record<string, unknown>)?.['edvVerify']` cast in
|
|
1326
|
+
* src/core/portfolio/backlog.ts — this only adds type safety/
|
|
1327
|
+
* discoverability, no behavior change.
|
|
1328
|
+
*/
|
|
1329
|
+
edvVerify?: boolean;
|
|
1330
|
+
/**
|
|
1331
|
+
* M185: gate for the `ashlrcode` (ac) autonomous executor path. DEFAULT
|
|
1332
|
+
* false (opt-in). Previously read only via an untyped
|
|
1333
|
+
* `(cfg.foundry as Record<string, unknown>)?.['ashlrcodeExecutor']` cast
|
|
1334
|
+
* in src/core/run/ashlrcode-engine.ts.
|
|
1335
|
+
*/
|
|
1336
|
+
ashlrcodeExecutor?: boolean;
|
|
1337
|
+
/**
|
|
1338
|
+
* Spec-contract acceptance-criteria checking gate. DEFAULT false
|
|
1339
|
+
* (disabled — emptyDetail short-circuit). Previously read only via an
|
|
1340
|
+
* untyped `(cfg?.foundry as Record<string, unknown>)?.['specContract']`
|
|
1341
|
+
* cast in src/core/run/spec-contract.ts.
|
|
1342
|
+
*/
|
|
1343
|
+
specContract?: boolean;
|
|
1344
|
+
/**
|
|
1345
|
+
* Inject a lightweight repo map into sandboxed-engine prompts. DEFAULT
|
|
1346
|
+
* false. Previously read only via an untyped
|
|
1347
|
+
* `(cfg.foundry as Record<string, unknown>)?.['repoMap']` cast in
|
|
1348
|
+
* src/core/run/sandboxed-engine.ts (sibling to `localization`, which
|
|
1349
|
+
* remains untyped/out of scope for this pass).
|
|
1350
|
+
*/
|
|
1351
|
+
repoMap?: boolean;
|
|
1352
|
+
/**
|
|
1353
|
+
* Repair-treatment maintenance pass toggle. DEFAULT true (opt-out via
|
|
1354
|
+
* false). Discovered live in ~/.ashlr/config.json during the M340b type
|
|
1355
|
+
* audit — previously read only via untyped
|
|
1356
|
+
* `(liveCfg.foundry as Record<string, unknown>)?.['proposalRepair']`
|
|
1357
|
+
* casts in src/core/daemon/loop.ts (3 sites) and src/core/fleet/status.ts.
|
|
1358
|
+
* NOT YET present in KNOWN_FOUNDRY_KEYS (src/core/effective-config.ts),
|
|
1359
|
+
* so a live config setting it currently trips a false-positive "unknown
|
|
1360
|
+
* foundry key" warning — flagged separately for that file's owner.
|
|
1361
|
+
*/
|
|
1362
|
+
proposalRepair?: boolean;
|
|
1313
1363
|
};
|
|
1314
1364
|
/**
|
|
1315
1365
|
* M24: optional autonomous-operator (daemon) tuning. When unset, the daemon
|
|
@@ -2778,6 +2828,18 @@ export interface DashboardSnapshot {
|
|
|
2778
2828
|
* making a second request. READ-ONLY and never carries secrets.
|
|
2779
2829
|
*/
|
|
2780
2830
|
fleet?: import('./fleet/status.js').FleetStatus;
|
|
2831
|
+
/**
|
|
2832
|
+
* Perf: OPTIONAL freshness marker for `fleet`. `buildFleetStatus` is now
|
|
2833
|
+
* served from a shared cache (see web/fleet-status-cache.ts) so concurrent
|
|
2834
|
+
* requests never trigger duplicate full recomputes. `stale: true` means
|
|
2835
|
+
* `fleet` is the last good value (still honestly reported, never silently
|
|
2836
|
+
* presented as current) while a background refresh is in flight; `ageMs`
|
|
2837
|
+
* is how old it is. Absent when the fleet section itself is absent.
|
|
2838
|
+
*/
|
|
2839
|
+
fleetFreshness?: {
|
|
2840
|
+
stale: boolean;
|
|
2841
|
+
ageMs: number;
|
|
2842
|
+
};
|
|
2781
2843
|
/**
|
|
2782
2844
|
* M29: OPTIONAL org-level portfolio roll-up. ABSENT on existing producers /
|
|
2783
2845
|
* tests (so they stay valid); populated by buildSnapshot when the v2 sources
|
|
@@ -2839,6 +2901,28 @@ export interface ProductionSummary {
|
|
|
2839
2901
|
harmful: number;
|
|
2840
2902
|
total: number;
|
|
2841
2903
|
};
|
|
2904
|
+
/**
|
|
2905
|
+
* Judge calls over the last 24h that produced NO real verdict — a parse
|
|
2906
|
+
* failure (unparseable LLM response, even after the one-shot retry) or a
|
|
2907
|
+
* network failure (the API call itself threw). judgeProposal() returns via
|
|
2908
|
+
* its `fallback()` helper BEFORE recordJudgeTrace() runs on either path
|
|
2909
|
+
* (see fleet/manager.ts), so these calls write no judge-trace row and are
|
|
2910
|
+
* invisible to judgeVerdicts24h — without this field a failed judge call
|
|
2911
|
+
* looks identical to a judge call that never happened.
|
|
2912
|
+
*
|
|
2913
|
+
* Sourced from the decisions ledger's distinct 'judge-parse-failure' /
|
|
2914
|
+
* 'judge-network-failure' judgeReasonCode (fleet/judge-decision-metadata.ts),
|
|
2915
|
+
* NOT from judge-trace — fallback verdicts are deliberately never written
|
|
2916
|
+
* to judge-trace because their synthetic all-3 scores would corrupt
|
|
2917
|
+
* judge-calibration.ts (darkCurrent/kappa/degradation-harness all read
|
|
2918
|
+
* every trace unconditionally). The decisions ledger already records both
|
|
2919
|
+
* failure kinds distinctly and separately from calibration's read path.
|
|
2920
|
+
*/
|
|
2921
|
+
judgeFailures24h: {
|
|
2922
|
+
parse: number;
|
|
2923
|
+
network: number;
|
|
2924
|
+
total: number;
|
|
2925
|
+
};
|
|
2842
2926
|
/** Auto-merges today: proposals that reached status 'applied' today. */
|
|
2843
2927
|
autoMergesToday: {
|
|
2844
2928
|
count: number;
|
|
@@ -3561,6 +3645,18 @@ export interface ProposalVerifyResult {
|
|
|
3561
3645
|
passed: boolean;
|
|
3562
3646
|
failed?: string[];
|
|
3563
3647
|
detail?: string;
|
|
3648
|
+
/**
|
|
3649
|
+
* Present only when passed:false. Distinguishes WHY verification failed:
|
|
3650
|
+
* Only explicit 'code' means the verifier ran and found a real problem with
|
|
3651
|
+
* the diff. An absent category is legacy/unknown and cannot authorize
|
|
3652
|
+
* permanent rejection. 'tool' means the verifier itself
|
|
3653
|
+
* could not be executed (missing binary, exit 127, spawn ENOENT — a
|
|
3654
|
+
* verifier-unavailable outcome, not a judgment on the diff). 'timeout' and
|
|
3655
|
+
* 'infra' are likewise infrastructure signals, not code signals. Consumers
|
|
3656
|
+
* that gate permanent rejection/auto-archival on verification failure MUST
|
|
3657
|
+
* check this before treating a failure as fatal to the proposal.
|
|
3658
|
+
*/
|
|
3659
|
+
failureCategory?: 'code' | 'tool' | 'timeout' | 'infra' | 'cancelled' | 'invalid-command';
|
|
3564
3660
|
ran?: Array<{
|
|
3565
3661
|
id?: string;
|
|
3566
3662
|
kind: 'typecheck' | 'lint' | 'build' | 'test';
|
|
@@ -5097,7 +5193,11 @@ export interface RunEstimate {
|
|
|
5097
5193
|
* M119: One entry in the append-only decisions ledger
|
|
5098
5194
|
* (~/.ashlr/decisions/YYYY-MM-DD.jsonl).
|
|
5099
5195
|
*/
|
|
5100
|
-
export type JudgeDecisionReasonCode = 'judge-ship-would-merge' | 'judge-ship-review-required' | 'judge-review' | 'judge-noise' | 'judge-harmful' | 'judge-verdict-unrecognized'
|
|
5196
|
+
export type JudgeDecisionReasonCode = 'judge-ship-would-merge' | 'judge-ship-review-required' | 'judge-review' | 'judge-noise' | 'judge-harmful' | 'judge-verdict-unrecognized'
|
|
5197
|
+
/** Judge responded but its output could not be parsed as a verdict (after retry). NOT a considered judgment. */
|
|
5198
|
+
| 'judge-parse-failure'
|
|
5199
|
+
/** The judge call itself failed (network/API/spawn error) — no model response was obtained. NOT a considered judgment. */
|
|
5200
|
+
| 'judge-network-failure';
|
|
5101
5201
|
export interface DecisionEntry {
|
|
5102
5202
|
/** ISO timestamp. */
|
|
5103
5203
|
ts: string;
|
package/dist/core/types.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAw5IH,MAAM,CAAC,MAAM,sCAAsC,GAAG,CAAC,CAAC"}
|
package/dist/core/web/api.d.ts
CHANGED
|
@@ -46,6 +46,8 @@ import type { AshlrConfig } from '../types.js';
|
|
|
46
46
|
* Each cleanup clears the interval and ends the response.
|
|
47
47
|
*/
|
|
48
48
|
export declare function drainSseConnections(): void;
|
|
49
|
+
/** Revoke only streams authenticated by one exact browser session. */
|
|
50
|
+
export declare function drainSseSession(sessionId: string): void;
|
|
49
51
|
/**
|
|
50
52
|
* Handle a single request if its URL matches an /api/* route.
|
|
51
53
|
*
|
|
@@ -57,4 +59,8 @@ export declare function drainSseConnections(): void;
|
|
|
57
59
|
export declare function handleApi(req: IncomingMessage, res: ServerResponse, cfg: AshlrConfig, ctx: {
|
|
58
60
|
token: string;
|
|
59
61
|
allowDispatch: boolean;
|
|
62
|
+
readSession?: {
|
|
63
|
+
id: string;
|
|
64
|
+
expiresAt: number;
|
|
65
|
+
};
|
|
60
66
|
}): Promise<boolean>;
|
package/dist/core/web/api.js
CHANGED
|
@@ -72,6 +72,7 @@ import { readPublicDaemonObservation, } from '../daemon/public-observation.js';
|
|
|
72
72
|
import { serviceStatus } from '../daemon/service.js';
|
|
73
73
|
import { daemonServiceInstallOptions } from '../daemon/service-config.js';
|
|
74
74
|
import { buildFleetStatus, readFleetDaemonStatus } from '../fleet/status.js';
|
|
75
|
+
import { getCachedFleetStatus, primeFleetStatusCache } from './fleet-status-cache.js';
|
|
75
76
|
// M61: Mission Control aggregator.
|
|
76
77
|
import { buildControlSnapshot } from './control.js';
|
|
77
78
|
// M90: Fleet-Activity panel.
|
|
@@ -93,9 +94,9 @@ const _sseCleanups = new Map();
|
|
|
93
94
|
* Register a cleanup callback for an SSE connection.
|
|
94
95
|
* Returns the id so it can be deregistered on close.
|
|
95
96
|
*/
|
|
96
|
-
function registerSse(cleanup) {
|
|
97
|
+
function registerSse(cleanup, sessionId) {
|
|
97
98
|
const id = randomBytes(8).toString('hex');
|
|
98
|
-
_sseCleanups.set(id, cleanup);
|
|
99
|
+
_sseCleanups.set(id, { cleanup, sessionId });
|
|
99
100
|
return id;
|
|
100
101
|
}
|
|
101
102
|
/** Remove a registered SSE cleanup. */
|
|
@@ -107,7 +108,7 @@ function deregisterSse(id) {
|
|
|
107
108
|
* Each cleanup clears the interval and ends the response.
|
|
108
109
|
*/
|
|
109
110
|
export function drainSseConnections() {
|
|
110
|
-
for (const cleanup of _sseCleanups.values()) {
|
|
111
|
+
for (const { cleanup } of _sseCleanups.values()) {
|
|
111
112
|
try {
|
|
112
113
|
cleanup();
|
|
113
114
|
}
|
|
@@ -118,6 +119,18 @@ export function drainSseConnections() {
|
|
|
118
119
|
_sseCleanups.clear();
|
|
119
120
|
sseHistoryProjection = null;
|
|
120
121
|
}
|
|
122
|
+
/** Revoke only streams authenticated by one exact browser session. */
|
|
123
|
+
export function drainSseSession(sessionId) {
|
|
124
|
+
for (const [id, entry] of _sseCleanups) {
|
|
125
|
+
if (entry.sessionId !== sessionId)
|
|
126
|
+
continue;
|
|
127
|
+
try {
|
|
128
|
+
entry.cleanup();
|
|
129
|
+
}
|
|
130
|
+
catch { /* best effort */ }
|
|
131
|
+
_sseCleanups.delete(id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
121
134
|
// ---------------------------------------------------------------------------
|
|
122
135
|
// SSE poll interval — bounded, not configurable by callers.
|
|
123
136
|
// ---------------------------------------------------------------------------
|
|
@@ -557,7 +570,10 @@ async function buildVisionMissionSnapshot(cfg) {
|
|
|
557
570
|
/** Write a JSON response. Never throws. */
|
|
558
571
|
function sendJson(res, status, body) {
|
|
559
572
|
try {
|
|
560
|
-
const
|
|
573
|
+
const boundedBody = body && typeof body === 'object' && 'error' in body
|
|
574
|
+
? { ...body, error: String(body.error).slice(0, 512) }
|
|
575
|
+
: body;
|
|
576
|
+
const payload = JSON.stringify(sanitizePublicJson(boundedBody) ?? null);
|
|
561
577
|
res.writeHead(status, {
|
|
562
578
|
'Content-Type': 'application/json',
|
|
563
579
|
'Cache-Control': 'no-store',
|
|
@@ -579,7 +595,8 @@ function sendJson(res, status, body) {
|
|
|
579
595
|
}
|
|
580
596
|
/** Write a 500 JSON error. Never throws. */
|
|
581
597
|
function send500(res, msg = 'internal error') {
|
|
582
|
-
|
|
598
|
+
void msg; // retained at call sites for private diagnostics; never disclose it.
|
|
599
|
+
sendJson(res, 500, { code: 'INTERNAL_ERROR', error: 'internal server error' });
|
|
583
600
|
}
|
|
584
601
|
/**
|
|
585
602
|
* Constant-time string comparison to defend against timing attacks.
|
|
@@ -743,12 +760,18 @@ function cachedSseHistoryProjection() {
|
|
|
743
760
|
* Concurrent connections are capped (SSE_MAX_CONNECTIONS) to bound timer/
|
|
744
761
|
* socket growth; excess connections get a 503.
|
|
745
762
|
*/
|
|
746
|
-
function handleSseEvents(req, res, cfg, allowDispatch) {
|
|
763
|
+
function handleSseEvents(req, res, cfg, allowDispatch, readSession) {
|
|
747
764
|
// Cap concurrent SSE connections to bound timer/socket growth.
|
|
748
765
|
if (_sseCleanups.size >= SSE_MAX_CONNECTIONS) {
|
|
749
766
|
sendJson(res, 503, { error: 'too many live connections' });
|
|
750
767
|
return;
|
|
751
768
|
}
|
|
769
|
+
// Install the response error listener before the first header/body write.
|
|
770
|
+
// The indirection observes the fully wired cleanup once registration is
|
|
771
|
+
// complete, while also preventing an early EventEmitter 'error' crash.
|
|
772
|
+
let cleanup = () => { };
|
|
773
|
+
if (typeof res.on === 'function')
|
|
774
|
+
res.on('error', () => cleanup());
|
|
752
775
|
// SSE headers — no buffering, keep-alive.
|
|
753
776
|
res.writeHead(200, {
|
|
754
777
|
'Content-Type': 'text/event-stream',
|
|
@@ -764,10 +787,26 @@ function handleSseEvents(req, res, cfg, allowDispatch) {
|
|
|
764
787
|
return; // Socket already gone.
|
|
765
788
|
}
|
|
766
789
|
// Helper: send one NAMED SSE event so the client's per-name listeners fire.
|
|
790
|
+
let backpressured = false;
|
|
791
|
+
let backpressureTimer;
|
|
792
|
+
let cleaned = false;
|
|
767
793
|
function sendNamed(event, payload) {
|
|
794
|
+
if (cleaned || backpressured)
|
|
795
|
+
return;
|
|
768
796
|
try {
|
|
769
797
|
const line = `event: ${event}\ndata: ${JSON.stringify(sanitizePublicJson(payload))}\n\n`;
|
|
770
|
-
res.write(line)
|
|
798
|
+
if (!res.write(line)) {
|
|
799
|
+
backpressured = true;
|
|
800
|
+
// Resolve cleanup when the timer fires; the initial snapshot can
|
|
801
|
+
// backpressure before the fully-wired cleanup closure is assigned.
|
|
802
|
+
backpressureTimer = setTimeout(() => cleanup(), 10_000);
|
|
803
|
+
res.once('drain', () => {
|
|
804
|
+
backpressured = false;
|
|
805
|
+
if (backpressureTimer)
|
|
806
|
+
clearTimeout(backpressureTimer);
|
|
807
|
+
backpressureTimer = undefined;
|
|
808
|
+
});
|
|
809
|
+
}
|
|
771
810
|
}
|
|
772
811
|
catch {
|
|
773
812
|
// Socket closed; cleanup will be triggered by 'close' event.
|
|
@@ -778,7 +817,7 @@ function handleSseEvents(req, res, cfg, allowDispatch) {
|
|
|
778
817
|
let updateInFlight = false;
|
|
779
818
|
// Emit one full update (runs, swarms, inbox, daemon slices).
|
|
780
819
|
async function emitUpdate() {
|
|
781
|
-
if (updateInFlight)
|
|
820
|
+
if (updateInFlight || backpressured)
|
|
782
821
|
return;
|
|
783
822
|
updateInFlight = true;
|
|
784
823
|
try {
|
|
@@ -809,7 +848,11 @@ function handleSseEvents(req, res, cfg, allowDispatch) {
|
|
|
809
848
|
catch {
|
|
810
849
|
// Keep the event stream alive with explicit unavailable provenance.
|
|
811
850
|
}
|
|
851
|
+
if (cleaned)
|
|
852
|
+
return;
|
|
812
853
|
const daemon = await readFreshDaemonObservation();
|
|
854
|
+
if (cleaned)
|
|
855
|
+
return;
|
|
813
856
|
sendNamed('daemon', legacyDaemonProjection(daemon));
|
|
814
857
|
sendNamed('daemon-observation', daemon);
|
|
815
858
|
// M90: fleet-activity liveness pulse — carry daemon tick count so the
|
|
@@ -856,8 +899,15 @@ function handleSseEvents(req, res, cfg, allowDispatch) {
|
|
|
856
899
|
}
|
|
857
900
|
}, SSE_POLL_MS);
|
|
858
901
|
// Cleanup: clear the interval and end the response.
|
|
859
|
-
|
|
902
|
+
cleanup = () => {
|
|
903
|
+
if (cleaned)
|
|
904
|
+
return;
|
|
905
|
+
cleaned = true;
|
|
860
906
|
clearInterval(intervalId);
|
|
907
|
+
if (backpressureTimer)
|
|
908
|
+
clearTimeout(backpressureTimer);
|
|
909
|
+
if (expiryTimer)
|
|
910
|
+
clearTimeout(expiryTimer);
|
|
861
911
|
deregisterSse(sseId);
|
|
862
912
|
try {
|
|
863
913
|
res.end();
|
|
@@ -866,7 +916,12 @@ function handleSseEvents(req, res, cfg, allowDispatch) {
|
|
|
866
916
|
// Already ended.
|
|
867
917
|
}
|
|
868
918
|
};
|
|
869
|
-
const sseId = registerSse(cleanup);
|
|
919
|
+
const sseId = registerSse(cleanup, readSession.id);
|
|
920
|
+
const expiryDelay = readSession.expiresAt - Date.now();
|
|
921
|
+
const expiryTimer = setTimeout(() => {
|
|
922
|
+
sendNamed('session-expired', { expired: true });
|
|
923
|
+
cleanup();
|
|
924
|
+
}, Math.max(0, expiryDelay));
|
|
870
925
|
// Clear on client disconnect.
|
|
871
926
|
req.on('close', cleanup);
|
|
872
927
|
req.on('error', cleanup);
|
|
@@ -1114,13 +1169,71 @@ export async function handleApi(req, res, cfg, ctx) {
|
|
|
1114
1169
|
sendJson(res, 200, pack);
|
|
1115
1170
|
return true;
|
|
1116
1171
|
}
|
|
1172
|
+
// Valid `?status=` values for GET /api/inbox — mirrors INBOX_STATUS_FILTERS
|
|
1173
|
+
// in src/cli/inbox.ts exactly. `?limit=` beyond the default is capped at
|
|
1174
|
+
// INBOX_LIST_HARD_MAX_LIMIT regardless of what the caller asks for.
|
|
1175
|
+
const INBOX_STATUS_QUERY_VALUES = new Set([
|
|
1176
|
+
'pending', 'approved', 'rejected', 'awaiting-host-merge', 'applied', 'failed', 'all',
|
|
1177
|
+
]);
|
|
1178
|
+
const INBOX_LIST_DEFAULT_LIMIT = 200;
|
|
1179
|
+
const INBOX_LIST_HARD_MAX_LIMIT = 1000;
|
|
1117
1180
|
// ── GET /api/inbox ───────────────────────────────────────────────────────
|
|
1118
|
-
// M23: read-only
|
|
1181
|
+
// M23: read-only proposals view. Defaults to pending-only, byte-identical
|
|
1182
|
+
// to the original response, unless an explicit status/since/limit opts
|
|
1183
|
+
// into the "history" view — mirrors `ashlr inbox --status/--since/--limit`
|
|
1184
|
+
// (src/cli/inbox.ts) so the web surface can finally browse rejected/
|
|
1185
|
+
// approved/etc, not just pending. listProposals never throws; every param
|
|
1186
|
+
// is validated against a fixed set / parsed strictly, and the history
|
|
1187
|
+
// response is capped (INBOX_LIST_DEFAULT_LIMIT / hard max) so opting into
|
|
1188
|
+
// filtering can never hand the browser an unbounded JSON payload.
|
|
1119
1189
|
if (path === '/api/inbox' && method === 'GET') {
|
|
1120
|
-
const
|
|
1190
|
+
const statusParam = getQueryParam(req.url ?? '/', 'status');
|
|
1191
|
+
const sinceParam = getQueryParam(req.url ?? '/', 'since');
|
|
1192
|
+
const limitParam = getQueryParam(req.url ?? '/', 'limit');
|
|
1193
|
+
if (statusParam !== undefined && !INBOX_STATUS_QUERY_VALUES.has(statusParam)) {
|
|
1194
|
+
sendJson(res, 400, {
|
|
1195
|
+
error: `status must be one of: ${[...INBOX_STATUS_QUERY_VALUES].join(', ')}; got: ${statusParam}`,
|
|
1196
|
+
});
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
let sinceMs;
|
|
1200
|
+
if (sinceParam !== undefined) {
|
|
1201
|
+
const parsed = Date.parse(sinceParam);
|
|
1202
|
+
if (Number.isNaN(parsed)) {
|
|
1203
|
+
sendJson(res, 400, { error: `since must be an ISO timestamp or YYYY-MM-DD, got: ${sinceParam}` });
|
|
1204
|
+
return true;
|
|
1205
|
+
}
|
|
1206
|
+
sinceMs = parsed;
|
|
1207
|
+
}
|
|
1208
|
+
let limit;
|
|
1209
|
+
if (limitParam !== undefined) {
|
|
1210
|
+
const n = /^[0-9]+$/.test(limitParam) ? Number.parseInt(limitParam, 10) : NaN;
|
|
1211
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1212
|
+
sendJson(res, 400, { error: `limit must be a positive integer, got: ${limitParam}` });
|
|
1213
|
+
return true;
|
|
1214
|
+
}
|
|
1215
|
+
limit = Math.min(n, INBOX_LIST_HARD_MAX_LIMIT);
|
|
1216
|
+
}
|
|
1217
|
+
// No query params at all -> the exact original call/shape (pending
|
|
1218
|
+
// only, unbounded). Any explicit status/since/limit opts into history.
|
|
1219
|
+
const filtering = statusParam !== undefined || sinceMs !== undefined || limit !== undefined;
|
|
1220
|
+
const effectiveStatus = statusParam ?? (sinceMs !== undefined ? 'all' : 'pending');
|
|
1221
|
+
const all = listProposals(); // newest-first; same full-directory read the pending-only call already did.
|
|
1222
|
+
let matched = effectiveStatus === 'all' ? all : all.filter((p) => p.status === effectiveStatus);
|
|
1223
|
+
if (sinceMs !== undefined) {
|
|
1224
|
+
const floor = sinceMs;
|
|
1225
|
+
matched = matched.filter((p) => {
|
|
1226
|
+
const t = Date.parse(p.createdAt);
|
|
1227
|
+
return !Number.isNaN(t) && t >= floor;
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
const shown = filtering ? matched.slice(0, limit ?? INBOX_LIST_DEFAULT_LIMIT) : matched;
|
|
1121
1231
|
sendJson(res, 200, {
|
|
1122
|
-
pending:
|
|
1123
|
-
|
|
1232
|
+
pending: filtering ? all.filter((p) => p.status === 'pending').length : shown.length,
|
|
1233
|
+
total: all.length,
|
|
1234
|
+
proposals: shown,
|
|
1235
|
+
truncated: matched.length > shown.length,
|
|
1236
|
+
filters: { status: effectiveStatus, since: sinceParam ?? null, limit: limit ?? null },
|
|
1124
1237
|
});
|
|
1125
1238
|
return true;
|
|
1126
1239
|
}
|
|
@@ -1136,7 +1249,34 @@ export async function handleApi(req, res, cfg, ctx) {
|
|
|
1136
1249
|
sendJson(res, 404, { error: `proposal not found: ${id}` });
|
|
1137
1250
|
return true;
|
|
1138
1251
|
}
|
|
1139
|
-
|
|
1252
|
+
// Join the decisions ledger (read-only, additive) so the operator can
|
|
1253
|
+
// see the judge's actual verdict/reasonCode for this proposal — a
|
|
1254
|
+
// judge-parse-failure/judge-network-failure is an infra failure, NOT a
|
|
1255
|
+
// considered judgment, and the ledger is the only place that
|
|
1256
|
+
// distinction lives (Proposal itself never carries judgeReasonCode).
|
|
1257
|
+
// Never invents a verdict: when the ledger read is degraded or no
|
|
1258
|
+
// decision row matches this id, sourceQuality says exactly that so the
|
|
1259
|
+
// UI's Epistemic primitive renders "unknown" instead of guessing.
|
|
1260
|
+
let decisionEvidence;
|
|
1261
|
+
try {
|
|
1262
|
+
const { readDecisionsDetailed } = await import('../fleet/decisions-ledger.js');
|
|
1263
|
+
const read = readDecisionsDetailed({ proposalId: id, limit: 25 });
|
|
1264
|
+
decisionEvidence = {
|
|
1265
|
+
sourceQuality: {
|
|
1266
|
+
sourceState: read.sourceState,
|
|
1267
|
+
complete: read.complete,
|
|
1268
|
+
...(read.stopReasons.length > 0 ? { reason: read.stopReasons.join(', ') } : {}),
|
|
1269
|
+
},
|
|
1270
|
+
decisions: read.decisions,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
decisionEvidence = {
|
|
1275
|
+
sourceQuality: { sourceState: 'degraded', complete: false, reason: 'ledger read failed' },
|
|
1276
|
+
decisions: [],
|
|
1277
|
+
};
|
|
1278
|
+
}
|
|
1279
|
+
sendJson(res, 200, { ...proposal, decisionEvidence });
|
|
1140
1280
|
return true;
|
|
1141
1281
|
}
|
|
1142
1282
|
// ── POST /api/inbox/:id/approve|reject (M32) ─────────────────────────
|
|
@@ -1251,6 +1391,10 @@ export async function handleApi(req, res, cfg, ctx) {
|
|
|
1251
1391
|
const retryable = !unsafeStorage && !mutation.quiesced && (mutation.ok ||
|
|
1252
1392
|
/fence unavailable|has not quiesced|outward mutation.*active|\bbusy\b/i.test(mutation.reason));
|
|
1253
1393
|
const fleet = await buildFleetStatus(cfg);
|
|
1394
|
+
// Prime the shared cache with this freshly-computed value so a
|
|
1395
|
+
// GET /api/fleet right after this mutation observes it immediately
|
|
1396
|
+
// instead of a pre-mutation cached value (see fleet-status-cache.ts).
|
|
1397
|
+
primeFleetStatusCache(cfg, fleet);
|
|
1254
1398
|
sendJson(res, retryable ? 409 : 500, {
|
|
1255
1399
|
ok: false,
|
|
1256
1400
|
action: paused ? 'pause' : 'resume',
|
|
@@ -1266,6 +1410,8 @@ export async function handleApi(req, res, cfg, ctx) {
|
|
|
1266
1410
|
return true;
|
|
1267
1411
|
}
|
|
1268
1412
|
const fleet = await buildFleetStatus(cfg);
|
|
1413
|
+
// See comment above — keep the shared cache in sync with the mutation.
|
|
1414
|
+
primeFleetStatusCache(cfg, fleet);
|
|
1269
1415
|
const service = serviceStatus(daemonServiceInstallOptions(cfg));
|
|
1270
1416
|
sendJson(res, 200, {
|
|
1271
1417
|
ok: true,
|
|
@@ -1282,8 +1428,14 @@ export async function handleApi(req, res, cfg, ctx) {
|
|
|
1282
1428
|
// proposals + merges + paused state). buildFleetStatus never throws; same
|
|
1283
1429
|
// no-auth read class as /api/daemon and /api/pulse.
|
|
1284
1430
|
if (path === '/api/fleet' && method === 'GET') {
|
|
1285
|
-
|
|
1286
|
-
|
|
1431
|
+
// M516: serve the cached FleetStatus VERBATIM. Freshness is deliberately
|
|
1432
|
+
// NOT spread in here: test/build-identity.test.ts pins that this route
|
|
1433
|
+
// performs no API-side recomposition, because a caller verifying
|
|
1434
|
+
// buildIdentity must be able to trust that what it received is exactly
|
|
1435
|
+
// what buildFleetStatus produced. Freshness is still reported on the
|
|
1436
|
+
// snapshot and control payloads, where their builders attach it.
|
|
1437
|
+
const cached = await getCachedFleetStatus(cfg);
|
|
1438
|
+
sendJson(res, 200, cached.status);
|
|
1287
1439
|
return true;
|
|
1288
1440
|
}
|
|
1289
1441
|
// ── GET /api/estimate ───────────────────────────────────────────────────
|
|
@@ -1345,7 +1497,11 @@ export async function handleApi(req, res, cfg, ctx) {
|
|
|
1345
1497
|
}
|
|
1346
1498
|
// ── GET /api/events (SSE) ────────────────────────────────────────────────
|
|
1347
1499
|
if (path === '/api/events' && method === 'GET') {
|
|
1348
|
-
|
|
1500
|
+
if (!ctx.readSession || ctx.readSession.expiresAt <= Date.now()) {
|
|
1501
|
+
sendJson(res, 401, { code: 'SESSION_REQUIRED', error: 'valid read session required' });
|
|
1502
|
+
return true;
|
|
1503
|
+
}
|
|
1504
|
+
handleSseEvents(req, res, cfg, ctx.allowDispatch, ctx.readSession);
|
|
1349
1505
|
return true;
|
|
1350
1506
|
}
|
|
1351
1507
|
// ── POST /api/run ────────────────────────────────────────────────────────
|