@ashlr/hub 3.2.6 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +290 -0
- package/README.md +4 -1
- package/dist/build-identity.json +2 -2
- package/dist/cli/activation.d.ts +41 -0
- package/dist/cli/activation.js +485 -0
- package/dist/cli/activation.js.map +1 -0
- 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 +37 -11
- package/dist/cli/completions.js.map +1 -1
- package/dist/cli/conductor-permit.d.ts +1 -0
- package/dist/cli/conductor-permit.js +122 -0
- package/dist/cli/conductor-permit.js.map +1 -0
- package/dist/cli/daemon.js +10 -3
- package/dist/cli/daemon.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/goals.d.ts +2 -0
- package/dist/cli/goals.js +90 -5
- package/dist/cli/goals.js.map +1 -1
- package/dist/cli/help.js +21 -3
- 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/index.d.ts +1 -0
- package/dist/cli/index.js +16 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/loop.js +26 -5
- package/dist/cli/loop.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/tui.d.ts +0 -6
- package/dist/cli/tui.js +14 -0
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/comms/elon-dialogue.js +16 -7
- package/dist/core/comms/elon-dialogue.js.map +1 -1
- package/dist/core/config.js +70 -0
- package/dist/core/config.js.map +1 -1
- package/dist/core/daemon/activation-permit.d.ts +357 -33
- package/dist/core/daemon/activation-permit.js +1585 -183
- package/dist/core/daemon/activation-permit.js.map +1 -1
- package/dist/core/daemon/goal-conductor-permit-operator.d.ts +75 -0
- package/dist/core/daemon/goal-conductor-permit-operator.js +842 -0
- package/dist/core/daemon/goal-conductor-permit-operator.js.map +1 -0
- package/dist/core/daemon/loop.d.ts +13 -1
- package/dist/core/daemon/loop.js +294 -29
- 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 +0 -4
- package/dist/core/daemon/service-install-authority.js +18 -3
- 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 +33 -0
- package/dist/core/fleet/automerge-pass.js.map +1 -1
- package/dist/core/fleet/decisions-ledger.js +35 -12
- 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/manager.d.ts +15 -1
- package/dist/core/fleet/manager.js +108 -30
- package/dist/core/fleet/manager.js.map +1 -1
- package/dist/core/fleet/post-merge-credit.d.ts +134 -9
- package/dist/core/fleet/post-merge-credit.js +350 -10
- package/dist/core/fleet/post-merge-credit.js.map +1 -1
- package/dist/core/fleet/quota.d.ts +74 -7
- package/dist/core/fleet/quota.js +688 -34
- package/dist/core/fleet/quota.js.map +1 -1
- package/dist/core/fleet/self-improve.d.ts +34 -1
- package/dist/core/fleet/self-improve.js +106 -1
- 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/status.js +7 -5
- package/dist/core/fleet/status.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/foundry/automerge-scope-policy.d.ts +2 -2
- package/dist/core/foundry/automerge-scope-policy.js +9 -2
- package/dist/core/foundry/automerge-scope-policy.js.map +1 -1
- package/dist/core/goals/advance.js +34 -5
- package/dist/core/goals/advance.js.map +1 -1
- package/dist/core/goals/conductor-quota.d.ts +38 -0
- package/dist/core/goals/conductor-quota.js +110 -0
- package/dist/core/goals/conductor-quota.js.map +1 -0
- package/dist/core/goals/conductor-target.d.ts +24 -0
- package/dist/core/goals/conductor-target.js +35 -0
- package/dist/core/goals/conductor-target.js.map +1 -0
- package/dist/core/goals/conductor.d.ts +15 -0
- package/dist/core/goals/conductor.js +116 -9
- package/dist/core/goals/conductor.js.map +1 -1
- package/dist/core/goals/store.d.ts +29 -5
- package/dist/core/goals/store.js +197 -30
- package/dist/core/goals/store.js.map +1 -1
- package/dist/core/goals/timestamp-repair.d.ts +50 -0
- package/dist/core/goals/timestamp-repair.js +740 -0
- package/dist/core/goals/timestamp-repair.js.map +1 -0
- package/dist/core/inbox/apply.js +5 -2
- package/dist/core/inbox/apply.js.map +1 -1
- package/dist/core/inbox/merge.d.ts +94 -1
- package/dist/core/inbox/merge.js +349 -16
- 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/integrations/pulse-sync.js +13 -4
- package/dist/core/integrations/pulse-sync.js.map +1 -1
- package/dist/core/learn/reflect.d.ts +42 -4
- package/dist/core/learn/reflect.js +56 -5
- package/dist/core/learn/reflect.js.map +1 -1
- package/dist/core/run/best-of-n.d.ts +18 -0
- package/dist/core/run/best-of-n.js +163 -18
- package/dist/core/run/best-of-n.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 +140 -60
- package/dist/core/run/orchestrator.js.map +1 -1
- package/dist/core/strategy/goal-planner.js +7 -1
- package/dist/core/strategy/goal-planner.js.map +1 -1
- package/dist/core/swarm/planner.d.ts +2 -2
- package/dist/core/swarm/planner.js +52 -5
- package/dist/core/swarm/planner.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 +123 -11
- package/dist/core/swarm/runner.js.map +1 -1
- package/dist/core/types.d.ts +139 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/web/api.js +105 -7
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/control.d.ts +20 -0
- package/dist/core/web/control.js +49 -34
- 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/next/assets/index-CY0U241s.css +1 -0
- package/dist/core/web/public/next/assets/index-Ct0mf8n4.js +69 -0
- package/dist/core/web/public/next/assets/index-Ct0mf8n4.js.map +1 -0
- package/dist/core/web/public/next/index.html +15 -0
- package/dist/release-dependency-inventory.json +1 -1
- package/dist/tui/app.js +2 -2
- package/dist/tui/app.js.map +1 -1
- package/dist/tui/render.d.ts +2 -0
- package/dist/tui/render.js +162 -4
- package/dist/tui/render.js.map +1 -1
- package/docs/RUNTIME_ACTIVATION_AUTHORITY.md +126 -21
- package/docs/contracts/CONTRACT-M521.md +102 -0
- package/package.json +20 -3
- package/schema/config.schema.json +800 -5
- package/node_modules/which/CHANGELOG.md +0 -166
|
@@ -1,11 +1,56 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Positive post-merge learning authority.
|
|
2
|
+
* Positive post-merge learning authority — the release protocol.
|
|
3
3
|
*
|
|
4
|
-
* A realized merge is an immutable operational fact, not proof that the
|
|
5
|
-
* remained beneficial.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A realized merge is an immutable operational fact, not proof that the
|
|
5
|
+
* change remained beneficial. This module is the ONLY place allowed to mint
|
|
6
|
+
* "post-merge credit release" authority, and it does so conservatively:
|
|
7
|
+
*
|
|
8
|
+
* 1. The proposal must carry an AUTHENTICATED realized-merge witness
|
|
9
|
+
* (authenticatedRealizedMergeOf — binds to this exact proposal via a
|
|
10
|
+
* local receipt HMAC or host reconciliation; shape alone is never
|
|
11
|
+
* authority).
|
|
12
|
+
* 2. At least MIN_POST_MERGE_OBSERVATION_WINDOW_MS must have elapsed since
|
|
13
|
+
* the merge was observed. The regression sentinel / outcome watcher
|
|
14
|
+
* (outcome-watcher.ts) sweeps merged proposals on a 6h throttle with a
|
|
15
|
+
* 7-day follow-up window; 14 days gives ~2x that follow-up window and
|
|
16
|
+
* dozens of scan cycles of headroom before we trust the silence.
|
|
17
|
+
* 3. The signed, observation-only post-merge-observations ledger
|
|
18
|
+
* (post-merge-observations.ts) must contain NO adverse record
|
|
19
|
+
* ('regressed' | 'reverted' | 'followed-up') for this exact
|
|
20
|
+
* (repo, proposalId, mergeCommit) event, and the ledger must have read
|
|
21
|
+
* back healthy/complete (a degraded read fails closed — we never treat
|
|
22
|
+
* "couldn't check" as "checked clean").
|
|
23
|
+
*
|
|
24
|
+
* That ledger is adverse-only (it never records a positive "we checked, it's
|
|
25
|
+
* clean" row), so eligibility here is inherently a "silence after a
|
|
26
|
+
* conservative window" heuristic, not a positive confirmation. That is a
|
|
27
|
+
* known, documented limitation — see sweepPostMergeCreditReleases' doc
|
|
28
|
+
* comment — not an oversight.
|
|
29
|
+
*
|
|
30
|
+
* MINTING: hasReleasedPostMergeCredit(labelBasis) verifies a self-describing,
|
|
31
|
+
* HMAC-signed token — NOT bare string equality against
|
|
32
|
+
* POST_MERGE_CREDIT_RELEASE_LABEL. This is deliberate: several downstream
|
|
33
|
+
* consumers persist labelBasis-shaped fields on structures that are, at least
|
|
34
|
+
* in tests and in caller-authored proposal/ledger data, fully attacker/
|
|
35
|
+
* test-controlled. A bare literal is trivially replayable; only a value
|
|
36
|
+
* carrying a valid local-key HMAC over the specific (repo, proposalId,
|
|
37
|
+
* mergeCommit) event can pass. isPostMergeCreditReleaseLabel() remains
|
|
38
|
+
* "structural recognition only" — matching the bare literal — for the
|
|
39
|
+
* existing defensive filters elsewhere that specifically reject the
|
|
40
|
+
* unauthenticated literal (e.g. skill-records.ts, decisions-ledger.ts).
|
|
8
41
|
*/
|
|
42
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
43
|
+
import { resolve } from 'node:path';
|
|
44
|
+
import { loadOrCreateKey, signSkillCardAttestation } from '../foundry/provenance.js';
|
|
45
|
+
import { authenticatedRealizedMergeOf } from '../inbox/realized-merge.js';
|
|
46
|
+
import { listProposalsDetailed } from '../inbox/store.js';
|
|
47
|
+
import { causalMetadataFromProposal } from '../learning/causal.js';
|
|
48
|
+
import { readDecisions, recordDecision } from './decisions-ledger.js';
|
|
49
|
+
import { postMergeObservationEventId, readPostMergeObservations } from './post-merge-observations.js';
|
|
50
|
+
import { skillCardContentHash } from './skill-attestation.js';
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// Structural constants
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
9
54
|
export const POST_MERGE_CREDIT_POLICY_VERSION = 'post-merge-credit-v1';
|
|
10
55
|
export const POST_MERGE_CREDIT_RELEASE_LABEL = 'post-merge-credit-release-v1';
|
|
11
56
|
/** Structural recognition only; this string is not release authority. */
|
|
@@ -13,11 +58,306 @@ export function isPostMergeCreditReleaseLabel(labelBasis) {
|
|
|
13
58
|
return labelBasis === POST_MERGE_CREDIT_RELEASE_LABEL;
|
|
14
59
|
}
|
|
15
60
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
61
|
+
* Minimum wall-clock time since the realized merge before credit MAY be
|
|
62
|
+
* released. See module doc for rationale (~2x the outcome-watcher follow-up
|
|
63
|
+
* window, dozens of throttled scan cycles of headroom).
|
|
64
|
+
*/
|
|
65
|
+
export const MIN_POST_MERGE_OBSERVATION_WINDOW_MS = 14 * 24 * 60 * 60 * 1000;
|
|
66
|
+
/** Domain-separated HMAC payload prefix for minted credit tokens. */
|
|
67
|
+
const CREDIT_TOKEN_DOMAIN = 'ashlr:post-merge-credit-release:v1';
|
|
68
|
+
/**
|
|
69
|
+
* Both segments are truncated to 24 hex chars (96 bits each) — plenty for
|
|
70
|
+
* MAC-forgery infeasibility and event-identifier collision-resistance at
|
|
71
|
+
* fleet scale, and DELIBERATELY short for two independent reasons discovered
|
|
72
|
+
* empirically (see test/post-merge-credit-release.test.ts):
|
|
73
|
+
* 1. decisions-ledger.ts's generic secret-scrubber (util/scrub.ts) redacts
|
|
74
|
+
* any bare 64+ char hex run as a likely API key/hash — a 64-char segment
|
|
75
|
+
* would be silently corrupted the moment it passed through
|
|
76
|
+
* recordDecision().
|
|
77
|
+
* 2. recordDecision() -> sanitizeDecisionEntry() unconditionally ends with
|
|
78
|
+
* normalizeDecisionLearningFields() (learning/causal.ts), which re-runs
|
|
79
|
+
* causalMetadata() and applies boundedText(labelBasis, 80) — ANY
|
|
80
|
+
* labelBasis over 80 chars is silently truncated, with no way to opt
|
|
81
|
+
* out from a caller outside that module. The full token
|
|
82
|
+
* ("post-merge-credit-release-v1:" (30 chars) + eventRef + ':' + mac)
|
|
83
|
+
* must fit in 80 chars total, so each hex segment is capped at 24
|
|
84
|
+
* (30 + 24 + 1 + 24 = 79).
|
|
85
|
+
* Neither constraint is owned by this module; 24 hex chars satisfies both
|
|
86
|
+
* with margin.
|
|
87
|
+
*/
|
|
88
|
+
const CREDIT_REF_HEX_CHARS = 24;
|
|
89
|
+
/** `post-merge-credit-release-v1:<event-ref-hex32>:<hmac-hex32>` */
|
|
90
|
+
const CREDIT_TOKEN_RE = new RegExp(`^post-merge-credit-release-v1:([a-f0-9]{${CREDIT_REF_HEX_CHARS}}):([a-f0-9]{${CREDIT_REF_HEX_CHARS}})$`);
|
|
91
|
+
/** Truncate a full sha256 eventId to the reference used inside the token. */
|
|
92
|
+
function creditEventRef(eventId) {
|
|
93
|
+
return eventId.slice(0, CREDIT_REF_HEX_CHARS);
|
|
94
|
+
}
|
|
95
|
+
function computeCreditMac(eventRef) {
|
|
96
|
+
try {
|
|
97
|
+
return createHmac('sha256', loadOrCreateKey())
|
|
98
|
+
.update(`${CREDIT_TOKEN_DOMAIN}:${eventRef}`)
|
|
99
|
+
.digest('hex')
|
|
100
|
+
.slice(0, CREDIT_REF_HEX_CHARS);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function equalHex(left, right) {
|
|
107
|
+
try {
|
|
108
|
+
const a = Buffer.from(left, 'hex');
|
|
109
|
+
const b = Buffer.from(right, 'hex');
|
|
110
|
+
return a.length === b.length && a.length > 0 && timingSafeEqual(a, b);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Verify that `labelBasis` is a validly-signed post-merge-credit release
|
|
118
|
+
* token minted by evaluatePostMergeCreditRelease. Pure, synchronous
|
|
119
|
+
* (host-local key read only), never throws. Deliberately rejects the bare
|
|
120
|
+
* POST_MERGE_CREDIT_RELEASE_LABEL literal and any other string that is not a
|
|
121
|
+
* correctly-formed, correctly-signed token — so raw ledgers, injected
|
|
122
|
+
* dependencies, and caller-controlled metadata cannot mint credit.
|
|
123
|
+
*/
|
|
124
|
+
export function hasReleasedPostMergeCredit(labelBasis) {
|
|
125
|
+
try {
|
|
126
|
+
if (typeof labelBasis !== 'string')
|
|
127
|
+
return false;
|
|
128
|
+
const match = CREDIT_TOKEN_RE.exec(labelBasis);
|
|
129
|
+
if (!match)
|
|
130
|
+
return false;
|
|
131
|
+
const eventRef = match[1];
|
|
132
|
+
const mac = match[2];
|
|
133
|
+
const expected = computeCreditMac(eventRef);
|
|
134
|
+
if (!expected)
|
|
135
|
+
return false;
|
|
136
|
+
return equalHex(mac, expected);
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Determine whether `proposal` is eligible for post-merge credit release
|
|
144
|
+
* RIGHT NOW, and mint the signed token when it is. Fail-closed on every
|
|
145
|
+
* ambiguous path (missing witness, unparseable timestamp, window not yet
|
|
146
|
+
* elapsed, degraded/incomplete observation-ledger read, any adverse
|
|
147
|
+
* observation for this exact merge event, signing failure). Never throws.
|
|
148
|
+
*
|
|
149
|
+
* This function does NOT persist anything — see sweepPostMergeCreditReleases
|
|
150
|
+
* for the write-back that records a minted label to the decisions ledger.
|
|
151
|
+
*/
|
|
152
|
+
export function evaluatePostMergeCreditRelease(proposal, opts = {}) {
|
|
153
|
+
try {
|
|
154
|
+
if (!proposal || typeof proposal.id !== 'string' || proposal.id.length === 0) {
|
|
155
|
+
return { eligible: false, label: null, reason: 'no-authenticated-merge-witness' };
|
|
156
|
+
}
|
|
157
|
+
const evidence = authenticatedRealizedMergeOf(proposal);
|
|
158
|
+
if (!evidence || typeof proposal.repo !== 'string' || proposal.repo.length === 0) {
|
|
159
|
+
return { eligible: false, label: null, reason: 'no-authenticated-merge-witness' };
|
|
160
|
+
}
|
|
161
|
+
const observedAtIso = evidence.source === 'github-host'
|
|
162
|
+
? evidence.reconciliation.observedAt
|
|
163
|
+
: evidence.observedAt;
|
|
164
|
+
const mergedAtMs = Date.parse(observedAtIso);
|
|
165
|
+
if (!Number.isFinite(mergedAtMs)) {
|
|
166
|
+
return { eligible: false, label: null, reason: 'invalid-merge-timestamp' };
|
|
167
|
+
}
|
|
168
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
169
|
+
if (nowMs < mergedAtMs || nowMs - mergedAtMs < MIN_POST_MERGE_OBSERVATION_WINDOW_MS) {
|
|
170
|
+
return { eligible: false, label: null, reason: 'observation-window-not-elapsed' };
|
|
171
|
+
}
|
|
172
|
+
let repoAbs;
|
|
173
|
+
try {
|
|
174
|
+
repoAbs = resolve(proposal.repo);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return { eligible: false, label: null, reason: 'no-authenticated-merge-witness' };
|
|
178
|
+
}
|
|
179
|
+
const eventId = postMergeObservationEventId({
|
|
180
|
+
repo: repoAbs,
|
|
181
|
+
proposalId: proposal.id,
|
|
182
|
+
mergeCommit: evidence.mergeCommitOid.toLowerCase(),
|
|
183
|
+
});
|
|
184
|
+
// Adverse-only ledger: absence of a row is the only "clean" signal it can
|
|
185
|
+
// ever give us. A degraded/incomplete read must NOT be treated as clean.
|
|
186
|
+
const read = readPostMergeObservations({ requireComplete: true });
|
|
187
|
+
if (read.sourceState === 'degraded' || !read.complete) {
|
|
188
|
+
return { eligible: false, label: null, reason: 'observation-ledger-degraded' };
|
|
189
|
+
}
|
|
190
|
+
if (read.observations.some((observation) => observation.eventId === eventId)) {
|
|
191
|
+
return { eligible: false, label: null, reason: 'adverse-outcome-observed' };
|
|
192
|
+
}
|
|
193
|
+
const eventRef = creditEventRef(eventId);
|
|
194
|
+
const mac = computeCreditMac(eventRef);
|
|
195
|
+
if (!mac)
|
|
196
|
+
return { eligible: false, label: null, reason: 'signing-unavailable' };
|
|
197
|
+
return {
|
|
198
|
+
eligible: true,
|
|
199
|
+
label: `${POST_MERGE_CREDIT_RELEASE_LABEL}:${eventRef}:${mac}`,
|
|
200
|
+
reason: 'clean-window-elapsed',
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return { eligible: false, label: null, reason: 'error' };
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// sweepPostMergeCreditReleases — periodic write-back
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
/**
|
|
211
|
+
* Bound on candidates inspected per sweep pass — mirrors outcome-watcher.ts's
|
|
212
|
+
* own bounding so a large backlog degrades gracefully instead of blocking.
|
|
213
|
+
*/
|
|
214
|
+
const MAX_SWEEP_CANDIDATES = 200;
|
|
215
|
+
/**
|
|
216
|
+
* Sweep applied proposals for newly-eligible post-merge credit and record it
|
|
217
|
+
* to the decisions ledger (a new 'merged' row with the signed labelBasis
|
|
218
|
+
* token — decisions-ledger.ts's own newest-row-wins semantics let this
|
|
219
|
+
* supersede the plain merge-fact row written at merge time). Idempotent: a
|
|
220
|
+
* proposal that already carries a released decision row is left alone.
|
|
221
|
+
*
|
|
222
|
+
* KNOWN LIMITATION (by design, not a bug): the post-merge-observations ledger
|
|
223
|
+
* only ever records ADVERSE outcomes. This sweep therefore releases credit on
|
|
224
|
+
* "no adverse record after a conservative window", not on a positive
|
|
225
|
+
* "actively confirmed clean" signal — there is no such positive signal
|
|
226
|
+
* anywhere in the fleet today. If a repo's outcome-watcher coverage were
|
|
227
|
+
* somehow disabled for a specific proposal's repo (not enrolled, or the local
|
|
228
|
+
* clone was deleted), this sweep cannot detect that and would release credit
|
|
229
|
+
* on elapsed time alone. The MIN_POST_MERGE_OBSERVATION_WINDOW_MS margin
|
|
230
|
+
* exists specifically to bound that risk; closing it fully would require
|
|
231
|
+
* outcome-watcher.ts (unowned here) to also persist a positive "swept clean"
|
|
232
|
+
* marker per merge event.
|
|
233
|
+
*
|
|
234
|
+
* SCALING NOTE: unlike outcome-watcher.ts, this sweep has no persistent
|
|
235
|
+
* cursor — it re-scans applied proposals from the top of listProposalsDetailed
|
|
236
|
+
* every pass, bounded by MAX_SWEEP_CANDIDATES. At small-to-moderate applied-
|
|
237
|
+
* proposal counts this converges fine (already-released proposals are
|
|
238
|
+
* skipped instantly via the decisions-ledger dedup check, so the cap mostly
|
|
239
|
+
* matters for proposals still awaiting their window). At very large scale a
|
|
240
|
+
* persistent cursor (mirroring monitoring-cursor.ts) would be needed to
|
|
241
|
+
* guarantee every proposal eventually gets swept; out of scope here.
|
|
242
|
+
*
|
|
243
|
+
* Never throws.
|
|
244
|
+
*/
|
|
245
|
+
export function sweepPostMergeCreditReleases(opts = {}) {
|
|
246
|
+
const result = {
|
|
247
|
+
scanned: 0, released: 0, skipped: 0, sourceComplete: true,
|
|
248
|
+
};
|
|
249
|
+
try {
|
|
250
|
+
const nowMs = opts.nowMs ?? Date.now();
|
|
251
|
+
let proposals;
|
|
252
|
+
if (typeof opts.listProposals === 'function') {
|
|
253
|
+
try {
|
|
254
|
+
proposals = opts.listProposals();
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
proposals = [];
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const read = listProposalsDetailed({ status: 'applied', requireComplete: true });
|
|
262
|
+
if (!read.complete || read.sourceState === 'degraded') {
|
|
263
|
+
result.sourceComplete = false;
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
proposals = read.proposals;
|
|
267
|
+
}
|
|
268
|
+
let inspected = 0;
|
|
269
|
+
for (const proposal of proposals) {
|
|
270
|
+
if (proposal.status !== 'applied')
|
|
271
|
+
continue;
|
|
272
|
+
if (inspected >= MAX_SWEEP_CANDIDATES) {
|
|
273
|
+
result.sourceComplete = false;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
inspected++;
|
|
277
|
+
result.scanned++;
|
|
278
|
+
try {
|
|
279
|
+
const existing = readDecisions({ proposalId: proposal.id, requireComplete: true });
|
|
280
|
+
const existingQuality = existing.sourceQuality;
|
|
281
|
+
if (existingQuality && (existingQuality.sourceState === 'degraded' || existingQuality.complete === false)) {
|
|
282
|
+
result.skipped++;
|
|
283
|
+
result.sourceComplete = false;
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
const alreadyReleased = existing.some((entry) => entry.action === 'merged' && hasReleasedPostMergeCredit(entry.labelBasis));
|
|
287
|
+
if (alreadyReleased)
|
|
288
|
+
continue;
|
|
289
|
+
const eligibility = evaluatePostMergeCreditRelease(proposal, { nowMs });
|
|
290
|
+
if (!eligibility.eligible || !eligibility.label) {
|
|
291
|
+
result.skipped++;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
const ts = new Date(nowMs).toISOString();
|
|
295
|
+
recordDecision({
|
|
296
|
+
ts,
|
|
297
|
+
proposalId: proposal.id,
|
|
298
|
+
// NOTE: labelBasis is intentionally NOT routed through
|
|
299
|
+
// causalMetadataFromProposal's `input.labelBasis` — causalMetadata()
|
|
300
|
+
// (learning/causal.ts) applies boundedText(value, 80) to that field,
|
|
301
|
+
// which would silently truncate our signed token (>80 chars) into
|
|
302
|
+
// an unverifiable fragment. Set it directly below instead, after the
|
|
303
|
+
// spread, so the full token survives.
|
|
304
|
+
...causalMetadataFromProposal(proposal, { ts, learningSource: 'outcome-record' }),
|
|
305
|
+
action: 'merged',
|
|
306
|
+
verdict: 'applied',
|
|
307
|
+
labelBasis: eligibility.label,
|
|
308
|
+
repo: proposal.repo ?? '',
|
|
309
|
+
engine: proposal.engineModel ?? '',
|
|
310
|
+
model: '',
|
|
311
|
+
});
|
|
312
|
+
result.released++;
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
result.skipped++;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// ---------------------------------------------------------------------------
|
|
325
|
+
// attestPostMergeCreditSkillCard — dedicated signing boundary
|
|
326
|
+
// ---------------------------------------------------------------------------
|
|
327
|
+
/**
|
|
328
|
+
* Sign a SkillCard that legitimately carries released post-merge credit.
|
|
329
|
+
*
|
|
330
|
+
* skill-attestation.ts's generic attestSkillCard() deliberately refuses any
|
|
331
|
+
* card for which hasReleasedPostMergeCredit(card.labelBasis) is true — its
|
|
332
|
+
* signature payload doesn't cover the credit-release claim, so it must not
|
|
333
|
+
* be the thing vouching for it. This module owns that proof, so it owns this
|
|
334
|
+
* signing boundary too: same crypto primitive (signSkillCardAttestation),
|
|
335
|
+
* same payload shape, so verifyAttestedSkillCard() on the read side still
|
|
336
|
+
* validates cards signed here — only the gating differs (this function signs
|
|
337
|
+
* ONLY when hasReleasedPostMergeCredit is true; attestSkillCard signs only
|
|
338
|
+
* when it is false).
|
|
339
|
+
*
|
|
340
|
+
* Never throws.
|
|
19
341
|
*/
|
|
20
|
-
export function
|
|
21
|
-
|
|
342
|
+
export function attestPostMergeCreditSkillCard(card) {
|
|
343
|
+
try {
|
|
344
|
+
if (!hasReleasedPostMergeCredit(card.labelBasis))
|
|
345
|
+
return null;
|
|
346
|
+
const diffHash = card.verification?.diffHash;
|
|
347
|
+
if (!card.proposalId || !diffHash)
|
|
348
|
+
return null;
|
|
349
|
+
const contentHash = skillCardContentHash(card);
|
|
350
|
+
const attestation = signSkillCardAttestation({
|
|
351
|
+
contentHash,
|
|
352
|
+
skillId: card.skillId,
|
|
353
|
+
revision: card.revision,
|
|
354
|
+
proposalId: card.proposalId,
|
|
355
|
+
diffHash,
|
|
356
|
+
});
|
|
357
|
+
return attestation ? { ...card, contentHash, attestation } : null;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
22
362
|
}
|
|
23
363
|
//# sourceMappingURL=post-merge-credit.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"post-merge-credit.js","sourceRoot":"","sources":["../../../src/core/fleet/post-merge-credit.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"post-merge-credit.js","sourceRoot":"","sources":["../../../src/core/fleet/post-merge-credit.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,30 +6,70 @@
|
|
|
6
6
|
* ledger records each dispatch and answers "how many in the last window?" and
|
|
7
7
|
* "are we still under the configured cap?".
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* Observation compatibility remains deliberately tolerant: load returns a fresh
|
|
10
|
+
* empty ledger on missing/corrupt state and recordUse remains best-effort. The
|
|
11
|
+
* authority-bearing reservation API is stricter:
|
|
12
|
+
* - One local-store lock serializes read/check/append publication.
|
|
13
|
+
* - Configured limits fail closed on ambiguous or unavailable persistence.
|
|
14
|
+
* - Reservation identities are hashed and idempotent across retries.
|
|
15
|
+
* - The replacement file and containing directory are fsynced before success.
|
|
13
16
|
* - mkdir -p the parent dir.
|
|
14
|
-
* - Bounded history (last
|
|
17
|
+
* - Bounded authority history (last 2000 events per known backend, max 30d).
|
|
15
18
|
* - Homedir re-resolved at call time so tests can relocate HOME.
|
|
16
19
|
*
|
|
17
20
|
* No new runtime deps; node builtins only. evalQuota mirrors evalBudget's
|
|
18
21
|
* three-level (ok/warn/over) semantics with a null-cap-disabled path.
|
|
19
22
|
*/
|
|
20
23
|
import type { AshlrConfig, EngineId } from '../types.js';
|
|
24
|
+
import { type LocalStoreLock } from './local-store-lock.js';
|
|
21
25
|
/** A single recorded backend dispatch. */
|
|
22
26
|
export interface FleetQuotaEvent {
|
|
23
27
|
/** Backend that was dispatched. */
|
|
24
28
|
backend: EngineId;
|
|
25
29
|
/** ISO timestamp of the dispatch. */
|
|
26
30
|
ts: string;
|
|
31
|
+
/** Opaque idempotency identity for an authority-bearing reservation. */
|
|
32
|
+
reservationIdHash?: string;
|
|
27
33
|
}
|
|
28
34
|
/** The persisted fleet quota ledger. */
|
|
29
35
|
export interface FleetQuota {
|
|
30
36
|
/** Bounded list of recent dispatch events (oldest first). */
|
|
31
37
|
events: FleetQuotaEvent[];
|
|
32
38
|
}
|
|
39
|
+
export interface FleetQuotaReservationRequest {
|
|
40
|
+
backend: EngineId;
|
|
41
|
+
dispatchId: string;
|
|
42
|
+
}
|
|
43
|
+
export interface FleetQuotaReservationReceipt {
|
|
44
|
+
backend: EngineId;
|
|
45
|
+
status: 'unlimited' | 'reserved';
|
|
46
|
+
used: number;
|
|
47
|
+
limit: number | null;
|
|
48
|
+
}
|
|
49
|
+
export type FleetQuotaReservationResult = {
|
|
50
|
+
kind: 'unlimited' | 'reserved';
|
|
51
|
+
launchAuthorized: true;
|
|
52
|
+
reservations: FleetQuotaReservationReceipt[];
|
|
53
|
+
} | {
|
|
54
|
+
kind: 'invalid' | 'duplicate' | 'conflict' | 'exhausted' | 'unavailable' | 'capacity';
|
|
55
|
+
launchAuthorized: false;
|
|
56
|
+
backend?: EngineId;
|
|
57
|
+
used?: number;
|
|
58
|
+
limit?: number;
|
|
59
|
+
/** True means the durable reservation exists, but this call must not launch. */
|
|
60
|
+
reservationConsumed?: boolean;
|
|
61
|
+
};
|
|
62
|
+
export interface FleetQuotaReservationOptions {
|
|
63
|
+
/** Bounded local lock wait. Defaults to 250ms. */
|
|
64
|
+
lockWaitMs?: number;
|
|
65
|
+
}
|
|
66
|
+
export type FleetQuotaAuthorityHealth = 'unlimited' | 'healthy' | 'invalid' | 'unavailable' | 'capacity';
|
|
67
|
+
interface FleetQuotaTestHooks {
|
|
68
|
+
now?: () => number;
|
|
69
|
+
releaseLock?: (lock: LocalStoreLock) => boolean;
|
|
70
|
+
}
|
|
71
|
+
/** Test-only fault injection; production callers must never configure this. */
|
|
72
|
+
export declare function setFleetQuotaTestHooksForTests(hooks?: FleetQuotaTestHooks): void;
|
|
33
73
|
/**
|
|
34
74
|
* Convert a window label to milliseconds. Extends rollup.ts's day-granularity
|
|
35
75
|
* vocabulary with sub-day windows used by rate limits. Unknown labels fall back
|
|
@@ -38,15 +78,37 @@ export interface FleetQuota {
|
|
|
38
78
|
export declare function windowToMs(window: string): number;
|
|
39
79
|
/** Absolute path to the fleet quota ledger file. */
|
|
40
80
|
export declare function fleetQuotaPath(): string;
|
|
81
|
+
/**
|
|
82
|
+
* Private authority ledger. It is deliberately separate from quota.json:
|
|
83
|
+
* quota.json remains backwards-compatible, best-effort operator telemetry,
|
|
84
|
+
* while this file is the fail-closed idempotency/capacity boundary.
|
|
85
|
+
*/
|
|
86
|
+
export declare function fleetQuotaReservationPath(): string;
|
|
87
|
+
/** Lock path used by the authority-bearing quota reservation transaction. */
|
|
88
|
+
export declare function fleetQuotaReservationLockPath(): string;
|
|
41
89
|
/**
|
|
42
90
|
* Read and parse fleetQuotaPath(). NEVER throws.
|
|
43
91
|
* Returns a fresh empty ledger when the file is missing or malformed.
|
|
44
92
|
*/
|
|
45
93
|
export declare function loadFleetQuota(): FleetQuota;
|
|
46
94
|
/**
|
|
47
|
-
* Append
|
|
95
|
+
* Append an actual dispatch-attempt event for `backend` (ts = now) and
|
|
96
|
+
* persist. Never throws. This is intentionally not launch authority; the
|
|
97
|
+
* strict reservation ledger above is separate so legacy 0644 telemetry files
|
|
98
|
+
* cannot block or overwrite authority state.
|
|
48
99
|
*/
|
|
49
100
|
export declare function recordUse(backend: EngineId): void;
|
|
101
|
+
export declare function reserveFleetQuotaUses(requests: readonly FleetQuotaReservationRequest[], cfg: AshlrConfig, options?: FleetQuotaReservationOptions): FleetQuotaReservationResult;
|
|
102
|
+
/** Single-dispatch convenience wrapper over the atomic batch transaction. */
|
|
103
|
+
export declare function reserveFleetQuotaUse(backend: EngineId, cfg: AshlrConfig, dispatchId: string, options?: FleetQuotaReservationOptions): FleetQuotaReservationResult;
|
|
104
|
+
/** Read-only conservative health projection for operator/status surfaces. */
|
|
105
|
+
export declare function inspectFleetQuotaAuthority(backend: EngineId, cfg: AshlrConfig, now?: number): FleetQuotaAuthorityHealth;
|
|
106
|
+
/**
|
|
107
|
+
* Quota standing derived from reserved authority slots, not attempt telemetry.
|
|
108
|
+
* This intentionally counts conservative pre-effect reservations that may not
|
|
109
|
+
* have reached a provider: those slots still consume the configured window.
|
|
110
|
+
*/
|
|
111
|
+
export declare function evalFleetQuotaAuthority(backend: EngineId, cfg: AshlrConfig, now?: number): 'ok' | 'warn' | 'over' | 'unlimited';
|
|
50
112
|
/**
|
|
51
113
|
* Count events for `backend` whose ts is within the last `windowMs`
|
|
52
114
|
* (ts >= now - windowMs). Reads the ledger internally; `now` is injectable for
|
|
@@ -58,7 +120,11 @@ export declare function usesInWindow(backend: EngineId, windowMs: number, now?:
|
|
|
58
120
|
*
|
|
59
121
|
* Mirrors evalBudget's null-cap-disabled logic: when no
|
|
60
122
|
* cfg.foundry.limits[backend] is configured, the backend is unlimited ⇒ always
|
|
61
|
-
* within limit.
|
|
123
|
+
* within limit. Healthy configured state follows reservation standing so a
|
|
124
|
+
* consumed slot can route to an eligible peer. Invalid, unavailable, or
|
|
125
|
+
* capacity-ambiguous authority deliberately remains advisory-eligible here so
|
|
126
|
+
* the final provider-effect reservation returns the structured fail-closed
|
|
127
|
+
* refusal instead of silently capability-downgrading to builtin. `now` is
|
|
62
128
|
* injectable for tests.
|
|
63
129
|
*/
|
|
64
130
|
export declare function withinLimit(backend: EngineId, cfg: AshlrConfig, now?: number): boolean;
|
|
@@ -68,3 +134,4 @@ export declare function withinLimit(backend: EngineId, cfg: AshlrConfig, now?: n
|
|
|
68
134
|
* cap is configured the backend is unlimited ⇒ always 'ok'. `now` is injectable.
|
|
69
135
|
*/
|
|
70
136
|
export declare function evalQuota(backend: EngineId, cfg: AshlrConfig, now?: number): 'ok' | 'warn' | 'over';
|
|
137
|
+
export {};
|