instar 1.3.545 → 1.3.547
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/dashboard/index.html +12 -1
- package/dashboard/mandates.js +88 -0
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +59 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +6 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/AuthorizationRequestStore.d.ts +138 -0
- package/dist/core/AuthorizationRequestStore.d.ts.map +1 -0
- package/dist/core/AuthorizationRequestStore.js +307 -0
- package/dist/core/AuthorizationRequestStore.js.map +1 -0
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +6 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/types.d.ts +10 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +6 -0
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/AgentServer.d.ts +3 -0
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +34 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/CapabilityIndex.d.ts.map +1 -1
- package/dist/server/CapabilityIndex.js +16 -0
- package/dist/server/CapabilityIndex.js.map +1 -1
- package/dist/server/routes.d.ts +21 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +167 -2
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/instar-dev-precommit.js +24 -1
- package/scripts/lib/operator-surface.mjs +30 -0
- package/src/data/builtin-manifest.json +47 -47
- package/src/scaffold/templates.ts +6 -0
- package/upgrades/1.3.546.md +24 -0
- package/upgrades/1.3.547.md +26 -0
- package/upgrades/side-effects/operator-authorization-request.md +65 -0
- package/upgrades/side-effects/ws52-incb-b3b-wire.md +46 -0
package/dashboard/index.html
CHANGED
|
@@ -845,6 +845,11 @@
|
|
|
845
845
|
.mnd-about p { margin: 8px 0 0; line-height: 1.55; }
|
|
846
846
|
.mnd-grant-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 4px; }
|
|
847
847
|
.mnd-grant-field { min-width: 130px; max-width: 100%; padding: 6px 8px; }
|
|
848
|
+
.mnd-approval-card { border: 1px solid var(--accent, #2f81f7); border-radius: 12px; padding: 16px 18px; margin-bottom: 12px; background: rgba(47,129,247,0.06); display: flex; flex-direction: column; gap: 10px; }
|
|
849
|
+
.mnd-approval-ask { font-size: 13px; opacity: 0.75; }
|
|
850
|
+
.mnd-approval-headline { font-size: 18px; font-weight: 600; line-height: 1.4; }
|
|
851
|
+
.mnd-approval-reason { font-size: 13px; opacity: 0.7; border-left: 3px solid var(--border); padding-left: 10px; }
|
|
852
|
+
.mnd-btn-quiet { opacity: 0.7; font-weight: 400; }
|
|
848
853
|
.mnd-notice { min-height: 20px; font-size: 14px; color: #3fb950; }
|
|
849
854
|
.mnd-issue-form { display: flex; flex-direction: column; gap: 14px; max-width: 640px; padding: 12px 0; }
|
|
850
855
|
.mnd-issue-form label { display: flex; flex-direction: column; gap: 4px; font-size: 15px; color: var(--text-bright); }
|
|
@@ -3416,12 +3421,16 @@
|
|
|
3416
3421
|
<p>These are your permission slips for autonomous agent-to-agent work — each one authorizes a specific, bounded task. The slip — never the agent — is the authorizer. Issuing and revoking require <strong>your dashboard PIN</strong>; an agent's own credentials are refused by design. With no slip issued, every delegated action is denied.</p>
|
|
3417
3422
|
</details>
|
|
3418
3423
|
<div id="mndNotice" class="mnd-notice" aria-live="polite"></div>
|
|
3424
|
+
<section class="ph-section" id="mndApprovalsSection" style="display:none">
|
|
3425
|
+
<h3 class="ph-h">Approvals waiting for you</h3>
|
|
3426
|
+
<div id="mndApprovals"></div>
|
|
3427
|
+
</section>
|
|
3419
3428
|
<section class="ph-section">
|
|
3420
3429
|
<h3 class="ph-h">Issued mandates</h3>
|
|
3421
3430
|
<div id="mndList"></div>
|
|
3422
3431
|
</section>
|
|
3423
3432
|
<details class="ph-detail" id="mndIssueDetails">
|
|
3424
|
-
<summary class="ph-detail-summary">
|
|
3433
|
+
<summary class="ph-detail-summary">Advanced — author a mandate by hand (PIN required)</summary>
|
|
3425
3434
|
<div class="mnd-issue-form">
|
|
3426
3435
|
<label>What is this permission slip for?
|
|
3427
3436
|
<span class="mnd-hint">A short name for the project the two agents may work on together, e.g. feedback-migration.</span>
|
|
@@ -9161,6 +9170,8 @@
|
|
|
9161
9170
|
if (!__mndController) {
|
|
9162
9171
|
const els = {
|
|
9163
9172
|
list: document.getElementById('mndList'),
|
|
9173
|
+
approvals: document.getElementById('mndApprovals'),
|
|
9174
|
+
approvalsSection: document.getElementById('mndApprovalsSection'),
|
|
9164
9175
|
audit: document.getElementById('mndAudit'),
|
|
9165
9176
|
notice: document.getElementById('mndNotice'),
|
|
9166
9177
|
stamp: document.getElementById('mndStamp'),
|
package/dashboard/mandates.js
CHANGED
|
@@ -225,6 +225,42 @@ export function renderMandates(list, slackUsers = []) {
|
|
|
225
225
|
}).join('');
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
+
/**
|
|
229
|
+
* "Approvals waiting for you" — the agent-proposes/operator-approves surface. The card
|
|
230
|
+
* text is the SERVER-authored `headline` (built from the structured proposal + the
|
|
231
|
+
* registered-user's real name) — we only esc() it, never compose authority from
|
|
232
|
+
* agent-supplied fields. The optional `reason` is rendered as a clearly-secondary,
|
|
233
|
+
* escaped note, never the headline (display-integrity standard). A request held on a
|
|
234
|
+
* different machine shows where to approve instead of a dead button.
|
|
235
|
+
*/
|
|
236
|
+
export function renderApprovals(requests) {
|
|
237
|
+
const list = (Array.isArray(requests) ? requests : []).filter((r) => r && r.status === 'pending');
|
|
238
|
+
if (list.length === 0) return '';
|
|
239
|
+
return list.map((r) => {
|
|
240
|
+
const headline = esc(r.headline || 'An approval is requested.');
|
|
241
|
+
const reason = r.reason
|
|
242
|
+
? `<div class="mnd-approval-reason">Echo's reason: ${esc(r.reason)}</div>` : '';
|
|
243
|
+
if (r.heldOnThisMachine === false) {
|
|
244
|
+
return `<div class="mnd-approval-card">
|
|
245
|
+
<div class="mnd-approval-ask">Echo is asking for your approval</div>
|
|
246
|
+
<div class="mnd-approval-headline">${headline}</div>
|
|
247
|
+
${reason}
|
|
248
|
+
<div class="mnd-dead-note">Asked on <strong>${esc(r.createdOnMachine || 'another machine')}</strong> — open that machine's dashboard to approve.</div>
|
|
249
|
+
</div>`;
|
|
250
|
+
}
|
|
251
|
+
return `<div class="mnd-approval-card">
|
|
252
|
+
<div class="mnd-approval-ask">Echo is asking for your approval</div>
|
|
253
|
+
<div class="mnd-approval-headline">${headline}</div>
|
|
254
|
+
${reason}
|
|
255
|
+
<div class="mnd-grant-row">
|
|
256
|
+
<input type="password" class="mnd-pin" data-approve-pin="${esc(r.id)}" placeholder="Your PIN" autocomplete="off" />
|
|
257
|
+
<button class="mnd-btn mnd-btn-primary" data-approve="${esc(r.id)}">Approve</button>
|
|
258
|
+
<button class="mnd-btn mnd-btn-quiet" data-deny="${esc(r.id)}">Decline</button>
|
|
259
|
+
</div>
|
|
260
|
+
</div>`;
|
|
261
|
+
}).join('');
|
|
262
|
+
}
|
|
263
|
+
|
|
228
264
|
export function renderAudit(payload) {
|
|
229
265
|
const entries = payload?.entries ?? [];
|
|
230
266
|
const chainOk = payload?.chain?.ok;
|
|
@@ -309,6 +345,16 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
309
345
|
const slackUsers = users.status === 200 && Array.isArray(users.body?.users) ? users.body.users : [];
|
|
310
346
|
els.list.innerHTML = renderMandates(mandates, slackUsers);
|
|
311
347
|
els.audit.innerHTML = renderAudit(audit.body);
|
|
348
|
+
// Pending authorization-requests — the "Approvals waiting for you" surface (the
|
|
349
|
+
// agent-proposes/operator-approves path). 503/absent → the section stays hidden.
|
|
350
|
+
if (els.approvals) {
|
|
351
|
+
const approvals = await fetchJson('/authorization-requests?status=pending').catch(() => ({ status: 0, body: null }));
|
|
352
|
+
const reqs = approvals.status === 200 && Array.isArray(approvals.body?.requests) ? approvals.body.requests : [];
|
|
353
|
+
const html = renderApprovals(reqs);
|
|
354
|
+
els.approvals.innerHTML = html;
|
|
355
|
+
if (els.approvalsSection) els.approvalsSection.style.display = html ? '' : 'none';
|
|
356
|
+
wireApprovalButtons();
|
|
357
|
+
}
|
|
312
358
|
if (els.stamp) els.stamp.textContent = 'updated ' + new Date().toLocaleTimeString();
|
|
313
359
|
// Nothing issued yet → the issue form IS the page's call to action;
|
|
314
360
|
// open it once rather than hiding it behind a collapsed <details>.
|
|
@@ -409,6 +455,48 @@ export function createController({ doc, els, fetchImpl }) {
|
|
|
409
455
|
});
|
|
410
456
|
}
|
|
411
457
|
|
|
458
|
+
// Approve / Decline a pending authorization-request — PIN-gated. The operator never
|
|
459
|
+
// authors anything here; they approve a server-authored card. The PIN is never retained.
|
|
460
|
+
function wireApprovalButtons() {
|
|
461
|
+
if (!els.approvals) return;
|
|
462
|
+
els.approvals.querySelectorAll('[data-approve]').forEach((btn) => {
|
|
463
|
+
btn.onclick = async () => {
|
|
464
|
+
const id = btn.getAttribute('data-approve');
|
|
465
|
+
const pinEl = els.approvals.querySelector(`[data-approve-pin="${id}"]`);
|
|
466
|
+
const pin = pinEl?.value ?? '';
|
|
467
|
+
if (!pin) { note('Type your dashboard PIN to approve — approving a grant is a human action.', true); return; }
|
|
468
|
+
btn.disabled = true;
|
|
469
|
+
try {
|
|
470
|
+
const { status, body } = await fetchJson(`/authorization-requests/${encodeURIComponent(id)}/approve`, {
|
|
471
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pin }),
|
|
472
|
+
});
|
|
473
|
+
if (pinEl) pinEl.value = '';
|
|
474
|
+
if (status === 200 || status === 201) { note('✓ Approved — the grant is now active.', 'success'); await refresh(); }
|
|
475
|
+
else note(`Not approved — the server refused: ${body?.error ?? `HTTP ${status}`}`, 'error');
|
|
476
|
+
} finally { btn.disabled = false; }
|
|
477
|
+
};
|
|
478
|
+
});
|
|
479
|
+
els.approvals.querySelectorAll('[data-deny]').forEach((btn) => {
|
|
480
|
+
btn.onclick = async () => {
|
|
481
|
+
const id = btn.getAttribute('data-deny');
|
|
482
|
+
const pinEl = els.approvals.querySelector(`[data-approve-pin="${id}"]`);
|
|
483
|
+
const pin = pinEl?.value ?? '';
|
|
484
|
+
if (!pin) { note('Type your dashboard PIN to decline — declining is a human action.', true); return; }
|
|
485
|
+
const denyReason = (typeof window !== 'undefined' && window.prompt) ? window.prompt('Why are you declining? (a short reason is required)') : 'declined';
|
|
486
|
+
if (!denyReason || !denyReason.trim()) { note('A short reason is required to decline.', true); return; }
|
|
487
|
+
btn.disabled = true;
|
|
488
|
+
try {
|
|
489
|
+
const { status, body } = await fetchJson(`/authorization-requests/${encodeURIComponent(id)}/deny`, {
|
|
490
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ pin, denyReason: denyReason.trim() }),
|
|
491
|
+
});
|
|
492
|
+
if (pinEl) pinEl.value = '';
|
|
493
|
+
if (status === 200) { note('Declined.', 'success'); await refresh(); }
|
|
494
|
+
else note(`Not declined — the server refused: ${body?.error ?? `HTTP ${status}`}`, 'error');
|
|
495
|
+
} finally { btn.disabled = false; }
|
|
496
|
+
};
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
412
500
|
// Validate EVERYTHING client-side and report every problem at once —
|
|
413
501
|
// never POST a request we know the server will refuse, and never let the
|
|
414
502
|
// operator discover a missing field one transient error at a time.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAiH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAm3CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAgCH,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAS3D,OAAO,EAAE,eAAe,EAAiC,MAAM,iCAAiC,CAAC;AAuBjG,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAiH7D,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAsBtD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,gBAAgB,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC1C,OAAO,CAUT;AAyID,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAm3CD,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,eAAe,EACzB,cAAc,EAAE,cAAc,EAC9B,YAAY,CAAC,EAAE,YAAY,EAC3B,WAAW,CAAC,EAAE,WAAW,EACzB,WAAW,CAAC,EAAE,WAAW,EACzB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,EAGvE,UAAU,CAAC,EAAE,MAAM,OAAO,8BAA8B,EAAE,WAAW,GAAG,IAAI,EAK5E,qBAAqB,CAAC,EAAE,MAAM,OAAO,gCAAgC,EAAE,kBAAkB,GAAG,IAAI,EAKhG,mBAAmB,CAAC,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,GACpD,IAAI,CA8eN;AA2lBD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA4lbtE;AAED,wBAAsB,UAAU,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAsDzE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE;IAAE,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAuD5E"}
|
package/dist/commands/server.js
CHANGED
|
@@ -8645,6 +8645,48 @@ export async function startServer(options) {
|
|
|
8645
8645
|
maxForcedPerWindow: config.subscriptionPool?.credentialRepointing?.maxForcedManualSwapsPerWindow,
|
|
8646
8646
|
forcedWindowMs: config.subscriptionPool?.credentialRepointing?.forcedManualSwapWindowMs,
|
|
8647
8647
|
});
|
|
8648
|
+
// B3b — the autonomous balancer (Increment B). Wraps the pure §2.4 decision core in a pass
|
|
8649
|
+
// loop and actuates via the SAME gated swap executor. isEnabled mirrors the location gate
|
|
8650
|
+
// (dev-gate-resolved AND the §2.10 env-token gate), and the executor's own dryRun keeps it a
|
|
8651
|
+
// dry-run dogfood on a dev agent (full decision loop + audit, ZERO writes) until dryRun:false.
|
|
8652
|
+
const { CredentialRebalancer } = await import('../core/CredentialRebalancer.js');
|
|
8653
|
+
const { mapSlots: credMapSlots, mapAccounts: credMapAccounts, resolveRebalancerConfig: credResolveBalancerConfig } = await import('../core/CredentialRebalancerSnapshot.js');
|
|
8654
|
+
const CRED_DEFAULT_SLOT = '~/.claude';
|
|
8655
|
+
const credentialRebalancer = new CredentialRebalancer({
|
|
8656
|
+
// Same gate as the location gate: dev-gate-resolved AND not env-token-refused.
|
|
8657
|
+
isEnabled: () => resolveDevAgentGate(config.subscriptionPool?.credentialRepointing?.enabled, config) &&
|
|
8658
|
+
!credentialEnvTokenGate.evaluate().refused,
|
|
8659
|
+
isDryRun: () => config.subscriptionPool?.credentialRepointing?.dryRun !== false,
|
|
8660
|
+
listSlots: () => credMapSlots(credentialLocationLedger.getAssignments(), { defaultSlot: CRED_DEFAULT_SLOT }),
|
|
8661
|
+
listAccounts: () => credMapAccounts(subscriptionPool.list(), Date.now()),
|
|
8662
|
+
resolveConfig: () => credResolveBalancerConfig({
|
|
8663
|
+
...(config.subscriptionPool?.credentialRepointing?.balancer ?? {}),
|
|
8664
|
+
slotCount: credentialLocationLedger.getAssignments().length,
|
|
8665
|
+
// Dry-run dogfood defaults: keep the account currently in ~/.claude as the desired
|
|
8666
|
+
// default (objective-0 keeps it alive). An explicit operator default-account config +
|
|
8667
|
+
// tmux-activity busyness for the drain target are refinements <!-- tracked: 20905 -->.
|
|
8668
|
+
desiredDefaultAccountId: credentialLocationLedger.tenantOf(CRED_DEFAULT_SLOT) ?? null,
|
|
8669
|
+
}),
|
|
8670
|
+
swap: async (a, b) => {
|
|
8671
|
+
const r = await credentialSwapExecutor.swap(a, b);
|
|
8672
|
+
return { ok: r.outcome === 'swapped' || r.outcome === 'dry-run', detail: r.reason };
|
|
8673
|
+
},
|
|
8674
|
+
emitDegraded: (m) => {
|
|
8675
|
+
try {
|
|
8676
|
+
DegradationReporter.getInstance().report({
|
|
8677
|
+
feature: 'CredentialRebalancer', primary: 'autonomous balancer pass',
|
|
8678
|
+
fallback: 'pass suspended (breaker / floor)', reason: m, impact: 'credential rebalancing degraded',
|
|
8679
|
+
});
|
|
8680
|
+
}
|
|
8681
|
+
catch { /* @silent-fallback-ok: degradation report is best-effort observability; a throwing reporter must never break the dark/dry-run pass */ }
|
|
8682
|
+
},
|
|
8683
|
+
emitAttention: (m) => {
|
|
8684
|
+
void credentialAuditEmit.attention({
|
|
8685
|
+
id: `credential-rebalancer-${credentialLocationLedger.version}`,
|
|
8686
|
+
title: 'Credential rebalancer', summary: m, category: 'credential-repointing', priority: 'NORMAL',
|
|
8687
|
+
});
|
|
8688
|
+
},
|
|
8689
|
+
});
|
|
8648
8690
|
const credentialRepointing = {
|
|
8649
8691
|
ledger: credentialLocationLedger,
|
|
8650
8692
|
swapExecutor: credentialSwapExecutor,
|
|
@@ -8652,6 +8694,7 @@ export async function startServer(options) {
|
|
|
8652
8694
|
audit: credentialAuditEmit,
|
|
8653
8695
|
levers: credentialManualLevers,
|
|
8654
8696
|
envTokenGate: credentialEnvTokenGate,
|
|
8697
|
+
rebalancer: credentialRebalancer,
|
|
8655
8698
|
readBlob: async (slot) => {
|
|
8656
8699
|
const raw = await defaultKeychainExec.readService(credSwapService(slot));
|
|
8657
8700
|
if (!raw)
|
|
@@ -8666,6 +8709,22 @@ export async function startServer(options) {
|
|
|
8666
8709
|
}
|
|
8667
8710
|
},
|
|
8668
8711
|
};
|
|
8712
|
+
// B3b — the periodic balancer pass. tick() is a strict no-op while the feature resolves dark
|
|
8713
|
+
// (so the timer can always run; the gate lives INSIDE tick()), and on a dev agent it runs the
|
|
8714
|
+
// full decision loop in dry-run (zero writes). REENTRANCY-GUARDED: a slow tick never overlaps
|
|
8715
|
+
// its successor. unref()'d so it never holds the process open. Interval clamped [1min, 60min].
|
|
8716
|
+
let credRebalancerTickInFlight = false;
|
|
8717
|
+
const credRebalancerPassMs = Math.min(3_600_000, Math.max(60_000, config.subscriptionPool?.credentialRepointing?.balancer?.passIntervalMs ?? 300_000));
|
|
8718
|
+
const credRebalancerTimer = setInterval(() => {
|
|
8719
|
+
if (credRebalancerTickInFlight)
|
|
8720
|
+
return; // reentrancy guard — skip if the prior tick is still running
|
|
8721
|
+
credRebalancerTickInFlight = true;
|
|
8722
|
+
void credentialRebalancer
|
|
8723
|
+
.tick()
|
|
8724
|
+
.catch((e) => console.warn(`[CredentialRebalancer] tick error: ${e instanceof Error ? e.message : String(e)}`))
|
|
8725
|
+
.finally(() => { credRebalancerTickInFlight = false; });
|
|
8726
|
+
}, credRebalancerPassMs);
|
|
8727
|
+
credRebalancerTimer.unref?.();
|
|
8669
8728
|
// QuotaPoller — per-account live quota reader (P1.2 of the Subscription &
|
|
8670
8729
|
// Auth Standard). Reads each account's 5h/weekly utilization + reset dates
|
|
8671
8730
|
// via the OAuth usage endpoint (transient per-account token, never persisted)
|