instar 1.3.276 → 1.3.278

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.
@@ -790,6 +790,35 @@
790
790
  display: none !important;
791
791
  }
792
792
 
793
+ /* Mandates tab */
794
+ .mnd-card { border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; margin-bottom: 12px; display: flex; flex-direction: column; gap: 8px; }
795
+ .mnd-card-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
796
+ .mnd-scope { font-size: 17px; font-weight: 600; color: var(--text-bright); }
797
+ .mnd-badge { font-size: 12px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); }
798
+ .mnd-ok { color: #3fb950; border-color: #3fb95055; }
799
+ .mnd-bad { color: #f85149; border-color: #f8514955; font-weight: 700; }
800
+ .mnd-deny { color: #d29922; border-color: #d2992255; }
801
+ .mnd-dead { color: var(--text-dim); }
802
+ .mnd-meta { font-size: 13px; color: var(--text-dim); }
803
+ .mnd-authorities { margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.7; }
804
+ .mnd-empty { font-size: 16px; color: var(--text-dim); line-height: 1.6; padding: 12px 0; }
805
+ .mnd-revoke-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
806
+ .mnd-pin { width: 110px; } .mnd-reason { flex: 1; min-width: 160px; }
807
+ .mnd-btn { padding: 6px 14px; cursor: pointer; }
808
+ .mnd-btn-danger { color: #f85149; }
809
+ .mnd-dead-note { font-size: 13px; color: var(--text-dim); font-style: italic; }
810
+ .mnd-notice { min-height: 20px; font-size: 14px; color: #3fb950; }
811
+ .mnd-notice-err { color: #f85149; }
812
+ .mnd-issue-form { display: flex; flex-direction: column; gap: 10px; max-width: 640px; padding: 12px 0; }
813
+ .mnd-issue-form label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; color: var(--text-dim); }
814
+ .mnd-issue-form input, .mnd-issue-form textarea { font: inherit; padding: 6px 8px; }
815
+ .mnd-issue-form textarea { font-family: ui-monospace, monospace; font-size: 12px; }
816
+ .mnd-table { width: 100%; border-collapse: collapse; font-size: 13px; }
817
+ .mnd-table th, .mnd-table td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
818
+ .mnd-reason-cell { color: var(--text-dim); max-width: 420px; }
819
+ .mnd-audit-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; }
820
+ .mnd-dim { font-size: 13px; color: var(--text-dim); }
821
+
793
822
  /* ── process-health tab (PROCESS-HEALTH-DASHBOARD-TAB-SPEC §4.1) ──
794
823
  Large, readable, calm. No monospace, no tables, no alarm colors.
795
824
  Measurable bars: headline ≥24px, section headers ≥20px, body ≥17px,
@@ -2652,6 +2681,7 @@
2652
2681
  <button class="tab" data-tab="process-health" onclick="switchTab('process-health')">Process Health</button>
2653
2682
  <button class="tab" data-tab="preferences-learning" onclick="switchTab('preferences-learning')">Preferences</button>
2654
2683
  <button class="tab" data-tab="machines" onclick="switchTab('machines')">Machines</button>
2684
+ <button class="tab" data-tab="mandates" onclick="switchTab('mandates')">Mandates</button>
2655
2685
  </nav>
2656
2686
  </div>
2657
2687
  <div class="vital-signs" id="vitalSigns">
@@ -3200,6 +3230,37 @@
3200
3230
  </details>
3201
3231
  </div>
3202
3232
 
3233
+ <!-- mandates tab (coordination-mandate spec, decision 2A — the operator's PIN-gated surface) -->
3234
+ <div id="mandatesPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
3235
+ <div style="display:flex;justify-content:space-between;align-items:baseline">
3236
+ <h2 class="ph-title">Coordination Mandates</h2>
3237
+ <div id="mndStamp" class="ph-stamp" aria-live="polite"></div>
3238
+ </div>
3239
+ <p class="ph-intro">Your bounded "permission slips" for autonomous agent-to-agent work. The slip — never the agent — is the authorizer. Issuing and revoking require <strong>your dashboard PIN</strong>; agent credentials alone are refused by design. With no slip issued, every delegated action is denied.</p>
3240
+ <div id="mndNotice" class="mnd-notice" aria-live="polite"></div>
3241
+ <section class="ph-section">
3242
+ <h3 class="ph-h">Issued mandates</h3>
3243
+ <div id="mndList"></div>
3244
+ </section>
3245
+ <details class="ph-detail">
3246
+ <summary class="ph-detail-summary">Issue a new mandate (PIN required)</summary>
3247
+ <div class="mnd-issue-form">
3248
+ <label>Scope <input type="text" id="mndIssueScope" value="feedback-migration" /></label>
3249
+ <label>Agent A fingerprint <input type="text" id="mndIssueAgentA" placeholder="full routing fingerprint" /></label>
3250
+ <label>Agent B fingerprint <input type="text" id="mndIssueAgentB" placeholder="full routing fingerprint" /></label>
3251
+ <label>Authorities (JSON) <textarea id="mndIssueAuthorities" rows="9" spellcheck="false"></textarea></label>
3252
+ <label>Expires <input type="datetime-local" id="mndIssueExpires" /></label>
3253
+ <label>Your dashboard PIN <input type="password" id="mndIssuePin" autocomplete="off" placeholder="typed at issuance, never stored" /></label>
3254
+ <button id="mndIssueBtn" class="mnd-btn">Issue mandate</button>
3255
+ </div>
3256
+ </details>
3257
+ <section class="ph-section">
3258
+ <h3 class="ph-h">Decision audit</h3>
3259
+ <p class="ph-h-sub">Every allow and deny, hash-chained. A broken chain means tampering — treat it as an incident.</p>
3260
+ <div id="mndAudit"></div>
3261
+ </section>
3262
+ </div>
3263
+
3203
3264
  <!-- preferences-learning tab (Correction & Preference Learning Sentinel §10 Slice-2) -->
3204
3265
  <div id="preferencesLearningPanel" class="ph-root" style="display:none;flex-direction:column;padding:28px;gap:24px;overflow-y:auto">
3205
3266
  <div style="display:flex;justify-content:space-between;align-items:baseline">
@@ -4543,6 +4604,13 @@
4543
4604
  onActivate: () => { if (typeof startMachines === 'function') startMachines(); },
4544
4605
  onDeactivate: () => { if (typeof stopMachines === 'function') stopMachines(); },
4545
4606
  },
4607
+ {
4608
+ id: 'mandates',
4609
+ panels: ['mandatesPanel'],
4610
+ display: ['flex'],
4611
+ onActivate: () => { if (typeof startMandates === 'function') startMandates(); },
4612
+ onDeactivate: () => { if (typeof stopMandates === 'function') stopMandates(); },
4613
+ },
4546
4614
  {
4547
4615
  id: 'secrets',
4548
4616
  panels: ['secretsPanel'],
@@ -8474,6 +8542,42 @@
8474
8542
  if (__phController) __phController.stop();
8475
8543
  }
8476
8544
 
8545
+ // ── Mandates tab (coordination-mandate spec, decision 2A) ──
8546
+ // Same lazy dynamic-import pattern as Process Health. The PIN typed in this
8547
+ // tab is sent once per action and never stored anywhere.
8548
+ let __mndController = null;
8549
+ let __mndModule = null;
8550
+ async function startMandates() {
8551
+ if (!__mndModule) {
8552
+ try { __mndModule = await import('/dashboard/mandates.js'); }
8553
+ catch (e) { console.error('[mandates] module load failed', e); return; }
8554
+ }
8555
+ if (!__mndController) {
8556
+ const els = {
8557
+ list: document.getElementById('mndList'),
8558
+ audit: document.getElementById('mndAudit'),
8559
+ notice: document.getElementById('mndNotice'),
8560
+ stamp: document.getElementById('mndStamp'),
8561
+ issueScope: document.getElementById('mndIssueScope'),
8562
+ issueAgentA: document.getElementById('mndIssueAgentA'),
8563
+ issueAgentB: document.getElementById('mndIssueAgentB'),
8564
+ issueAuthorities: document.getElementById('mndIssueAuthorities'),
8565
+ issueExpires: document.getElementById('mndIssueExpires'),
8566
+ issuePin: document.getElementById('mndIssuePin'),
8567
+ issueBtn: document.getElementById('mndIssueBtn'),
8568
+ };
8569
+ const fetchImpl = (url, opts = {}) => fetch(url, {
8570
+ ...opts,
8571
+ headers: { 'Authorization': `Bearer ${token}`, ...(opts.headers || {}) },
8572
+ });
8573
+ __mndController = __mndModule.createController({ doc: document, els, fetchImpl });
8574
+ }
8575
+ __mndController.start();
8576
+ }
8577
+ function stopMandates() {
8578
+ if (__mndController) __mndController.stop();
8579
+ }
8580
+
8477
8581
  // ── Preferences tab (Correction & Preference Learning Sentinel §10 Slice-2) ──
8478
8582
  // Same lazy dynamic-import pattern as Process Health: the controller lives in
8479
8583
  // this (classic) scope so it can read `token`; the module ships the pure
@@ -0,0 +1,205 @@
1
+ /**
2
+ * mandates.js — the Mandates dashboard tab (coordination-mandate spec, decision 2A).
3
+ *
4
+ * The OPERATOR's surface for the Coordination Mandate engine: see every mandate
5
+ * (with live authorship verification), issue a new one, revoke one, and read the
6
+ * hash-chained decision audit. Issuance + revocation require the dashboard PIN —
7
+ * typed here at action time, sent once, NEVER stored (no localStorage, no module
8
+ * state). The Bearer token alone cannot perform either; that is the engine's
9
+ * design, and this tab is the human-authenticated surface it points at.
10
+ *
11
+ * Same shape as process-health.js / preferences-learning.js: pure renderers +
12
+ * createController({ doc, els, fetchImpl }) so index.html stays a thin shim.
13
+ */
14
+
15
+ const REFRESH_MS = 30_000;
16
+
17
+ // The A/A/B first-mandate shape, prefilled so the operator edits rather than authors.
18
+ const AUTHORITIES_TEMPLATE = JSON.stringify([
19
+ { action: 'exchange-read-credential', bounds: { credentialScope: 'read-only', onMachine: true } },
20
+ { action: 'sign-code-review', bounds: { artifact: 'migration-port', mutual: true } },
21
+ ], null, 2);
22
+
23
+ function esc(s) {
24
+ return String(s ?? '').replace(/[&<>"']/g, (c) => ({
25
+ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;',
26
+ }[c]));
27
+ }
28
+
29
+ function shortFp(fp) {
30
+ const s = String(fp ?? '');
31
+ return s.length > 12 ? s.slice(0, 8) + '…' : s;
32
+ }
33
+
34
+ function fmtWhen(iso) {
35
+ if (!iso) return '—';
36
+ try {
37
+ const d = new Date(iso);
38
+ return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
39
+ } catch { return String(iso); }
40
+ }
41
+
42
+ export function renderMandates(list) {
43
+ if (!Array.isArray(list) || list.length === 0) {
44
+ return '<div class="mnd-empty">No mandates issued. The gate is deny-by-default: every delegated agent action refuses until you issue one.</div>';
45
+ }
46
+ return list.map((m) => {
47
+ const expired = Date.parse(m.expiresAt) < Date.now();
48
+ const state = m.revoked ? 'revoked' : expired ? 'expired' : 'active';
49
+ const stateCls = state === 'active' ? 'mnd-ok' : 'mnd-dead';
50
+ const authBadge = m.authorshipValid
51
+ ? '<span class="mnd-badge mnd-ok">authorship verified</span>'
52
+ : '<span class="mnd-badge mnd-bad">AUTHORSHIP INVALID</span>';
53
+ const authorities = (m.authorities || []).map((a) =>
54
+ `<li><code>${esc(a.action)}</code> — bounds <code>${esc(JSON.stringify(a.bounds))}</code>${a.requiresCondition ? ` — requires <code>${esc(a.requiresCondition)}</code>` : ''}</li>`,
55
+ ).join('');
56
+ const revokeUi = state === 'active'
57
+ ? `<div class="mnd-revoke-row">
58
+ <input type="password" class="mnd-pin" data-revoke-pin="${esc(m.id)}" placeholder="PIN" autocomplete="off" />
59
+ <input type="text" class="mnd-reason" data-revoke-reason="${esc(m.id)}" placeholder="reason" />
60
+ <button class="mnd-btn mnd-btn-danger" data-revoke="${esc(m.id)}">Revoke</button>
61
+ </div>`
62
+ : `<div class="mnd-dead-note">${m.revoked ? `revoked ${fmtWhen(m.revoked.at)} — ${esc(m.revoked.reason)}` : `expired ${fmtWhen(m.expiresAt)}`}</div>`;
63
+ return `<div class="mnd-card">
64
+ <div class="mnd-card-head">
65
+ <span class="mnd-scope">${esc(m.scope)}</span>
66
+ <span class="mnd-badge ${stateCls}">${state}</span>
67
+ ${authBadge}
68
+ </div>
69
+ <div class="mnd-meta">id <code>${esc(m.id)}</code> · agents <code>${esc(shortFp(m.agents?.[0]))}</code> + <code>${esc(shortFp(m.agents?.[1]))}</code> · by ${esc(m.author)} · expires ${fmtWhen(m.expiresAt)}</div>
70
+ <ul class="mnd-authorities">${authorities}</ul>
71
+ ${revokeUi}
72
+ </div>`;
73
+ }).join('');
74
+ }
75
+
76
+ export function renderAudit(payload) {
77
+ const entries = payload?.entries ?? [];
78
+ const chainOk = payload?.chain?.ok;
79
+ const chainBadge = chainOk === false
80
+ ? '<span class="mnd-badge mnd-bad">CHAIN BROKEN — possible tampering</span>'
81
+ : '<span class="mnd-badge mnd-ok">chain verified</span>';
82
+ if (entries.length === 0) {
83
+ return `<div class="mnd-audit-head">${chainBadge}</div><div class="mnd-empty">No decisions recorded yet.</div>`;
84
+ }
85
+ const rows = entries.slice(-25).reverse().map((e) =>
86
+ `<tr>
87
+ <td>${fmtWhen(e.ts)}</td>
88
+ <td><span class="mnd-badge ${e.decision === 'allow' ? 'mnd-ok' : 'mnd-deny'}">${esc(e.decision)}</span></td>
89
+ <td><code>${esc(e.action)}</code></td>
90
+ <td><code>${esc(shortFp(e.agentFp))}</code></td>
91
+ <td class="mnd-reason-cell">${esc(e.reason)}</td>
92
+ </tr>`,
93
+ ).join('');
94
+ return `<div class="mnd-audit-head">${chainBadge}<span class="mnd-dim">${entries.length} total decisions · newest first</span></div>
95
+ <table class="mnd-table"><thead><tr><th>when</th><th>decision</th><th>action</th><th>agent</th><th>reason</th></tr></thead><tbody>${rows}</tbody></table>`;
96
+ }
97
+
98
+ export function createController({ doc, els, fetchImpl }) {
99
+ let timer = null;
100
+ let running = false;
101
+
102
+ async function fetchJson(url, opts) {
103
+ const res = await fetchImpl(url, opts);
104
+ let body = null;
105
+ try { body = await res.json(); } catch { body = null; }
106
+ return { status: res.status, body };
107
+ }
108
+
109
+ function note(msg, isError) {
110
+ if (!els.notice) return;
111
+ els.notice.textContent = msg;
112
+ els.notice.className = isError ? 'mnd-notice mnd-notice-err' : 'mnd-notice';
113
+ if (msg) setTimeout(() => { if (els.notice.textContent === msg) els.notice.textContent = ''; }, 8000);
114
+ }
115
+
116
+ async function refresh() {
117
+ try {
118
+ const [mand, audit] = await Promise.all([
119
+ fetchJson('/mandate'),
120
+ fetchJson('/mandate/audit?limit=200'),
121
+ ]);
122
+ if (mand.status === 503) {
123
+ els.list.innerHTML = '<div class="mnd-empty">Mandate engine unavailable on this server (older version or init failure).</div>';
124
+ els.audit.innerHTML = '';
125
+ if (els.stamp) els.stamp.textContent = '';
126
+ return;
127
+ }
128
+ els.list.innerHTML = renderMandates(mand.body?.mandates);
129
+ els.audit.innerHTML = renderAudit(audit.body);
130
+ if (els.stamp) els.stamp.textContent = 'updated ' + new Date().toLocaleTimeString();
131
+ wireRevokeButtons();
132
+ } catch (e) {
133
+ note('refresh failed: ' + (e?.message ?? e), true);
134
+ }
135
+ }
136
+
137
+ function wireRevokeButtons() {
138
+ els.list.querySelectorAll('[data-revoke]').forEach((btn) => {
139
+ btn.onclick = async () => {
140
+ const id = btn.getAttribute('data-revoke');
141
+ const pinEl = els.list.querySelector(`[data-revoke-pin="${id}"]`);
142
+ const reasonEl = els.list.querySelector(`[data-revoke-reason="${id}"]`);
143
+ const pin = pinEl?.value ?? '';
144
+ if (!pin) { note('Type your dashboard PIN to revoke — revocation is a human action.', true); return; }
145
+ btn.disabled = true;
146
+ try {
147
+ const { status, body } = await fetchJson(`/mandate/${encodeURIComponent(id)}/revoke`, {
148
+ method: 'POST',
149
+ headers: { 'Content-Type': 'application/json' },
150
+ body: JSON.stringify({ pin, reason: reasonEl?.value || 'operator revocation (dashboard)' }),
151
+ });
152
+ if (pinEl) pinEl.value = ''; // never retain the PIN
153
+ if (status === 200) { note(`Mandate ${id} revoked.`); await refresh(); }
154
+ else note(body?.error ?? `revoke failed (${status})`, true);
155
+ } finally { btn.disabled = false; }
156
+ };
157
+ });
158
+ }
159
+
160
+ async function issue() {
161
+ const pin = els.issuePin?.value ?? '';
162
+ if (!pin) { note('Type your dashboard PIN — issuance is a human action; agent credentials are refused.', true); return; }
163
+ let authorities;
164
+ try { authorities = JSON.parse(els.issueAuthorities?.value || '[]'); }
165
+ catch { note('Authorities must be valid JSON (array of { action, bounds }).', true); return; }
166
+ const payload = {
167
+ pin,
168
+ scope: els.issueScope?.value?.trim(),
169
+ agents: [els.issueAgentA?.value?.trim(), els.issueAgentB?.value?.trim()],
170
+ authorities,
171
+ expiresAt: els.issueExpires?.value ? new Date(els.issueExpires.value).toISOString() : '',
172
+ };
173
+ els.issueBtn.disabled = true;
174
+ try {
175
+ const { status, body } = await fetchJson('/mandate/issue', {
176
+ method: 'POST',
177
+ headers: { 'Content-Type': 'application/json' },
178
+ body: JSON.stringify(payload),
179
+ });
180
+ if (els.issuePin) els.issuePin.value = ''; // never retain the PIN
181
+ if (status === 201) {
182
+ note(`Mandate ${body?.mandate?.id ?? ''} issued.`);
183
+ await refresh();
184
+ } else {
185
+ note(body?.error ?? `issue failed (${status})`, true);
186
+ }
187
+ } finally { els.issueBtn.disabled = false; }
188
+ }
189
+
190
+ function start() {
191
+ if (running) return;
192
+ running = true;
193
+ if (els.issueAuthorities && !els.issueAuthorities.value) els.issueAuthorities.value = AUTHORITIES_TEMPLATE;
194
+ if (els.issueBtn) els.issueBtn.onclick = issue;
195
+ refresh();
196
+ timer = setInterval(refresh, REFRESH_MS);
197
+ }
198
+
199
+ function stop() {
200
+ running = false;
201
+ if (timer) { clearInterval(timer); timer = null; }
202
+ }
203
+
204
+ return { start, stop, refresh };
205
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA4SH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAqxDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAojRtE;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"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/commands/server.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AA4SH,UAAU,YAAY;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;2DACuD;IACvD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAqxDD,wBAAsB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CA8jRtE;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"}
@@ -9460,6 +9460,17 @@ export async function startServer(options) {
9460
9460
  ? provisioner.provisionAll()
9461
9461
  : Promise.resolve([]),
9462
9462
  localKeyPaths: () => secretSyncMod.secretKeyPaths(readSecrets()),
9463
+ // Vault readability probe — NEVER mask a decrypt failure as "empty"
9464
+ // (the 2026-06-05 bifurcation hid behind localKeyPaths: []).
9465
+ vaultStatus: () => {
9466
+ try {
9467
+ const secrets = provStore.read();
9468
+ return { status: Object.keys(secrets).length > 0 ? 'ok' : 'empty' };
9469
+ }
9470
+ catch (err) {
9471
+ return { status: 'decrypt-failed', error: err instanceof Error ? err.message : String(err) };
9472
+ }
9473
+ },
9463
9474
  syncTargets: onlinePeers,
9464
9475
  };
9465
9476
  if (_secretSyncPushEnabled) {