instar 1.3.519 → 1.3.521
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 +40 -10
- package/dashboard/mandates.js +115 -34
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +75 -1
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +17 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/ReplicatedRecordEnvelope.d.ts +380 -0
- package/dist/core/ReplicatedRecordEnvelope.d.ts.map +1 -0
- package/dist/core/ReplicatedRecordEnvelope.js +464 -0
- package/dist/core/ReplicatedRecordEnvelope.js.map +1 -0
- package/dist/core/stateSyncConfig.d.ts +94 -0
- package/dist/core/stateSyncConfig.d.ts.map +1 -0
- package/dist/core/stateSyncConfig.js +126 -0
- package/dist/core/stateSyncConfig.js.map +1 -0
- package/dist/core/types.d.ts +59 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/package.json +1 -1
- package/scripts/instar-dev-precommit.js +46 -0
- package/scripts/lib/operator-surface.mjs +54 -0
- package/skills/instar-dev/templates/side-effects-artifact.md +15 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.521.md +69 -0
- package/upgrades/side-effects/multi-machine-replicated-store-foundation-step2-journal-kind.md +104 -0
- package/upgrades/side-effects/operator-surface-quality-and-mandates-redesign.md +200 -0
package/dashboard/index.html
CHANGED
|
@@ -815,18 +815,34 @@
|
|
|
815
815
|
.mnd-bad { color: #f85149; border-color: #f8514955; font-weight: 700; }
|
|
816
816
|
.mnd-deny { color: #d29922; border-color: #d2992255; }
|
|
817
817
|
.mnd-dead { color: var(--text-dim); }
|
|
818
|
-
|
|
819
|
-
|
|
818
|
+
/* Internals (id, fingerprints, scope slug, action slugs) are SUPPORT
|
|
819
|
+
metadata only — de-emphasized, never the headline (Operator-Surface
|
|
820
|
+
Quality item 2). */
|
|
821
|
+
.mnd-meta { font-size: 12px; color: var(--text-dim); }
|
|
822
|
+
/* The card's plain-language headline sentence — the primary content. */
|
|
823
|
+
.mnd-summary { margin: 0; font-size: 15px; line-height: 1.5; color: var(--text-bright); }
|
|
820
824
|
.mnd-empty { font-size: 16px; color: var(--text-dim); line-height: 1.6; padding: 12px 0; }
|
|
821
|
-
.mnd-revoke-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
|
|
825
|
+
.mnd-revoke-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 4px; }
|
|
822
826
|
.mnd-pin { width: 110px; } .mnd-reason { flex: 1; min-width: 160px; }
|
|
823
827
|
.mnd-btn { padding: 6px 14px; cursor: pointer; }
|
|
824
|
-
.mnd-btn-
|
|
828
|
+
.mnd-btn-primary { font-weight: 600; }
|
|
829
|
+
/* Destructive action: quiet by design, never featured (item 3). */
|
|
830
|
+
.mnd-btn-danger { color: #f85149; font-weight: 400; }
|
|
825
831
|
.mnd-dead-note { font-size: 13px; color: var(--text-dim); font-style: italic; }
|
|
826
832
|
.mnd-grants-head { font-size: 13px; color: var(--text-dim); }
|
|
827
|
-
.mnd-grants { margin: 0; padding-left: 18px; font-size:
|
|
828
|
-
.mnd-grant-
|
|
829
|
-
|
|
833
|
+
.mnd-grants { margin: 0; padding-left: 18px; font-size: 14px; line-height: 1.7; }
|
|
834
|
+
.mnd-grant-id { color: var(--text-dim); font-size: 12px; }
|
|
835
|
+
/* The grant form is the card's PRIMARY action — an open, titled block, never
|
|
836
|
+
a collapsed toggle (Operator-Surface Quality item 1). */
|
|
837
|
+
.mnd-grant-block { border-top: 1px solid var(--border); padding-top: 10px; margin-top: 2px; }
|
|
838
|
+
.mnd-grant-title { font-size: 14px; font-weight: 600; color: var(--text-bright); margin-bottom: 6px; }
|
|
839
|
+
.mnd-grant-sep { font-size: 13px; color: var(--text-dim); }
|
|
840
|
+
/* Revoke is demoted to a quiet, collapsed secondary control (item 3). */
|
|
841
|
+
.mnd-revoke-details { font-size: 13px; margin-top: 4px; }
|
|
842
|
+
.mnd-revoke-summary { cursor: pointer; color: var(--text-dim); font-size: 13px; }
|
|
843
|
+
.mnd-about { font-size: 14px; color: var(--text-dim); margin-top: -8px; }
|
|
844
|
+
.mnd-about summary { cursor: pointer; }
|
|
845
|
+
.mnd-about p { margin: 8px 0 0; line-height: 1.55; }
|
|
830
846
|
.mnd-grant-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin: 8px 0 4px; }
|
|
831
847
|
.mnd-grant-field { min-width: 130px; max-width: 100%; padding: 6px 8px; }
|
|
832
848
|
.mnd-notice { min-height: 20px; font-size: 14px; color: #3fb950; }
|
|
@@ -842,9 +858,19 @@
|
|
|
842
858
|
.mnd-notice-ok { border: 1px solid #15803d; background: rgba(21, 128, 61, 0.12); color: #86efac; padding: 12px 14px; border-radius: 8px; font-size: 17px; font-weight: 600; line-height: 1.5; }
|
|
843
859
|
.mnd-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
844
860
|
.mnd-table th, .mnd-table td { text-align: left; padding: 6px 8px; border-bottom: 1px solid var(--border); vertical-align: top; }
|
|
845
|
-
.mnd-reason-cell { color: var(--text-dim);
|
|
846
|
-
.mnd-audit-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; }
|
|
861
|
+
.mnd-reason-cell { color: var(--text-dim); }
|
|
862
|
+
.mnd-audit-head { display: flex; gap: 12px; align-items: center; margin-bottom: 8px; flex-wrap: wrap; }
|
|
847
863
|
.mnd-dim { font-size: 13px; color: var(--text-dim); }
|
|
864
|
+
/* Mobile-first (item 5): at phone width the 5-column audit table stacks into
|
|
865
|
+
labelled rows so the reason — the useful column — is never truncated. */
|
|
866
|
+
@media (max-width: 640px) {
|
|
867
|
+
.mnd-table thead { position: absolute; left: -9999px; }
|
|
868
|
+
.mnd-table tr { display: block; border: 1px solid var(--border); border-radius: 8px; margin-bottom: 10px; padding: 6px 4px; }
|
|
869
|
+
.mnd-table td { display: flex; justify-content: space-between; gap: 12px; border-bottom: none; padding: 4px 8px; }
|
|
870
|
+
.mnd-table td::before { content: attr(data-label); font-weight: 600; color: var(--text-dim); flex: 0 0 auto; }
|
|
871
|
+
.mnd-reason-cell { text-align: right; word-break: break-word; }
|
|
872
|
+
.mnd-grant-field, .mnd-pin, .mnd-reason { width: 100%; min-width: 0; flex: 1 1 100%; }
|
|
873
|
+
}
|
|
848
874
|
|
|
849
875
|
/* ── process-health tab (PROCESS-HEALTH-DASHBOARD-TAB-SPEC §4.1) ──
|
|
850
876
|
Large, readable, calm. No monospace, no tables, no alarm colors.
|
|
@@ -3384,7 +3410,11 @@
|
|
|
3384
3410
|
<h2 class="ph-title">Coordination Mandates</h2>
|
|
3385
3411
|
<div id="mndStamp" class="ph-stamp" aria-live="polite"></div>
|
|
3386
3412
|
</div>
|
|
3387
|
-
<p class="ph-intro">
|
|
3413
|
+
<p class="ph-intro">Permission slips you sign with your PIN to let your agents do bounded work together.</p>
|
|
3414
|
+
<details class="mnd-about">
|
|
3415
|
+
<summary>What is this?</summary>
|
|
3416
|
+
<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
|
+
</details>
|
|
3388
3418
|
<div id="mndNotice" class="mnd-notice" aria-live="polite"></div>
|
|
3389
3419
|
<section class="ph-section">
|
|
3390
3420
|
<h3 class="ph-h">Issued mandates</h3>
|
package/dashboard/mandates.js
CHANGED
|
@@ -61,12 +61,72 @@ function fmtWhen(iso) {
|
|
|
61
61
|
} catch { return String(iso); }
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
/** The registry name for a Slack id, or the id itself when we don't know them. */
|
|
65
|
+
function userName(slackUserId, slackUsers) {
|
|
65
66
|
const u = (slackUsers || []).find((x) => x.slackUserId === slackUserId);
|
|
66
|
-
return u ?
|
|
67
|
+
return u ? u.name : slackUserId;
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
|
|
70
|
+
// Operator-Surface Quality (constitution): the card speaks plain language, never
|
|
71
|
+
// the slugs/JSON the data model stores. These maps turn the stored enum/bounds
|
|
72
|
+
// into the sentence a non-engineer would say. Unknown values fall back to a
|
|
73
|
+
// de-slugified phrase — never a raw token dumped at the operator.
|
|
74
|
+
|
|
75
|
+
// Both the coordination-mandate authority actions AND the RolePolicy floor
|
|
76
|
+
// actions, in plain language. Returns the RAW human string; callers esc() it.
|
|
77
|
+
const ACTION_PHRASES = {
|
|
78
|
+
'exchange-read-credential': 'exchange a read-only credential',
|
|
79
|
+
'sign-code-review': 'co-sign a code review',
|
|
80
|
+
'money-movement': 'move money',
|
|
81
|
+
'prod-deploy': 'deploy to production',
|
|
82
|
+
'credential-access': 'use a credential',
|
|
83
|
+
'destructive-data': 'run a destructive data operation',
|
|
84
|
+
'external-send': 'send something externally',
|
|
85
|
+
'grant-authority': 'grant authority to others',
|
|
86
|
+
};
|
|
87
|
+
function humanAction(slug) {
|
|
88
|
+
const s = String(slug ?? '');
|
|
89
|
+
return ACTION_PHRASES[s] || s.replace(/-/g, ' ');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** "you" when the authorizer is the operator-via-PIN; otherwise the raw name. */
|
|
93
|
+
function humanAuthorizer(authorizedBy) {
|
|
94
|
+
const s = String(authorizedBy ?? '');
|
|
95
|
+
return /operator|dashboard pin/i.test(s) ? 'you' : s;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A short title from the operator's scope name (de-slugified, not the raw slug). */
|
|
99
|
+
function humanScope(scope) {
|
|
100
|
+
const s = String(scope ?? '').trim();
|
|
101
|
+
if (!s) return 'Permission slip';
|
|
102
|
+
const words = s.replace(/[-_]+/g, ' ');
|
|
103
|
+
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Join phrases the way a person speaks them: "a, b and c". */
|
|
107
|
+
function joinHuman(parts) {
|
|
108
|
+
if (parts.length === 0) return '';
|
|
109
|
+
if (parts.length === 1) return parts[0];
|
|
110
|
+
return parts.slice(0, -1).join(', ') + ' and ' + parts[parts.length - 1];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* One plain sentence describing what the mandate authorizes + when it ends —
|
|
115
|
+
* the card's primary, human-language headline (no slugs, no JSON, no
|
|
116
|
+
* fingerprints). Each interpolated action is escaped at the call site.
|
|
117
|
+
*/
|
|
118
|
+
function humanSummary(m) {
|
|
119
|
+
const acts = (m.authorities || []).map((a) => esc(humanAction(a.action)));
|
|
120
|
+
const agents = (m.agents || []).filter(Boolean);
|
|
121
|
+
const who = agents.length >= 2 ? 'two agents' : 'an agent';
|
|
122
|
+
const when = fmtWhen(m.expiresAt);
|
|
123
|
+
if (acts.length === 0) {
|
|
124
|
+
return `A permission slip for ${who} that delegates no actions yet. Expires ${when}.`;
|
|
125
|
+
}
|
|
126
|
+
return `Lets ${who} ${joinHuman(acts)}. Expires ${when}.`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** The grants a mandate already carries, in plain operator language. */
|
|
70
130
|
export function renderGrants(m, slackUsers) {
|
|
71
131
|
const grants = Array.isArray(m.grants) ? m.grants : [];
|
|
72
132
|
if (grants.length === 0) return '';
|
|
@@ -75,16 +135,22 @@ export function renderGrants(m, slackUsers) {
|
|
|
75
135
|
const badge = expired
|
|
76
136
|
? '<span class="mnd-badge mnd-dead">expired</span>'
|
|
77
137
|
: '<span class="mnd-badge mnd-ok">active</span>';
|
|
78
|
-
|
|
138
|
+
const name = userName(g.grantedTo, slackUsers);
|
|
139
|
+
// The Slack id is support metadata, never the headline — shown muted only
|
|
140
|
+
// when it differs from the name we render (an unknown id IS the name).
|
|
141
|
+
const idDetail = name !== g.grantedTo ? ` <span class="mnd-grant-id">(${esc(g.grantedTo)})</span>` : '';
|
|
142
|
+
return `<li>${badge} <strong>${esc(name)}</strong>${idDetail} can ${esc(humanAction(g.floorAction))} until ${fmtWhen(g.expiresAt)} — authorized by ${esc(humanAuthorizer(g.authorizedBy))}</li>`;
|
|
79
143
|
}).join('');
|
|
80
|
-
return `<div class="mnd-grants-head">
|
|
144
|
+
return `<div class="mnd-grants-head">Who this slip already lets act:</div><ul class="mnd-grants">${rows}</ul>`;
|
|
81
145
|
}
|
|
82
146
|
|
|
83
147
|
/**
|
|
84
|
-
* The add-grant form for an ACTIVE mandate
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
148
|
+
* The add-grant form for an ACTIVE mandate — the card's PRIMARY action, always
|
|
149
|
+
* visible (Operator-Surface Quality: lead with the primary action, never behind
|
|
150
|
+
* a toggle). Mobile-first (the 2026-06-12 lesson, instar#1080): the operator
|
|
151
|
+
* PICKS a person and a duration — the only thing typed is the PIN. A free-text
|
|
152
|
+
* Slack-id input appears only when the user registry has nobody to offer. Action
|
|
153
|
+
* labels read in plain language; the slug is the option VALUE the server needs.
|
|
88
154
|
*/
|
|
89
155
|
export function renderGrantForm(m, slackUsers) {
|
|
90
156
|
const users = (slackUsers || []).filter((u) => u.slackUserId);
|
|
@@ -94,21 +160,23 @@ export function renderGrantForm(m, slackUsers) {
|
|
|
94
160
|
}</select>`
|
|
95
161
|
: `<input type="text" class="mnd-grant-field" data-grant-user="${esc(m.id)}" placeholder="Slack user id (e.g. U0…)" />`;
|
|
96
162
|
const actionField = `<select class="mnd-grant-field" data-grant-action="${esc(m.id)}">${
|
|
97
|
-
FLOOR_ACTIONS.map((a) => `<option value="${a}"${a === 'prod-deploy' ? ' selected' : ''}>${a}</option>`).join('')
|
|
163
|
+
FLOOR_ACTIONS.map((a) => `<option value="${a}"${a === 'prod-deploy' ? ' selected' : ''}>${esc(humanAction(a))}</option>`).join('')
|
|
98
164
|
}</select>`;
|
|
99
165
|
const durationField = `<select class="mnd-grant-field" data-grant-duration="${esc(m.id)}">${
|
|
100
166
|
GRANT_DURATIONS.map((d) => `<option value="${d.minutes}"${d.minutes === 60 ? ' selected' : ''}>${d.label}</option>`).join('')
|
|
101
167
|
}</select>`;
|
|
102
|
-
return `<
|
|
168
|
+
return `<div class="mnd-grant-block">
|
|
169
|
+
<div class="mnd-grant-title">Grant a person an action</div>
|
|
103
170
|
<div class="mnd-grant-row">
|
|
104
171
|
${granteeField}
|
|
105
172
|
${actionField}
|
|
173
|
+
<span class="mnd-grant-sep">for</span>
|
|
106
174
|
${durationField}
|
|
107
|
-
<input type="password" class="mnd-pin" data-grant-pin="${esc(m.id)}" placeholder="PIN" autocomplete="off" />
|
|
108
|
-
<button class="mnd-btn" data-grant="${esc(m.id)}">Grant</button>
|
|
175
|
+
<input type="password" class="mnd-pin" data-grant-pin="${esc(m.id)}" placeholder="Your PIN" autocomplete="off" />
|
|
176
|
+
<button class="mnd-btn mnd-btn-primary" data-grant="${esc(m.id)}">Grant</button>
|
|
109
177
|
</div>
|
|
110
|
-
<span class="mnd-hint">
|
|
111
|
-
</
|
|
178
|
+
<span class="mnd-hint">Lets the person you pick take that one action for the window you choose. Confirmed with your PIN — never stored. A grant can never outlive this slip, and revoking the slip ends it.</span>
|
|
179
|
+
</div>`;
|
|
112
180
|
}
|
|
113
181
|
|
|
114
182
|
export function renderMandates(list, slackUsers = []) {
|
|
@@ -119,30 +187,39 @@ export function renderMandates(list, slackUsers = []) {
|
|
|
119
187
|
const expired = Date.parse(m.expiresAt) < Date.now();
|
|
120
188
|
const state = m.revoked ? 'revoked' : expired ? 'expired' : 'active';
|
|
121
189
|
const stateCls = state === 'active' ? 'mnd-ok' : 'mnd-dead';
|
|
190
|
+
const stateLabel = state === 'active' ? 'active' : state === 'revoked' ? 'revoked' : 'expired';
|
|
122
191
|
const authBadge = m.authorshipValid
|
|
123
192
|
? '<span class="mnd-badge mnd-ok">authorship verified</span>'
|
|
124
193
|
: '<span class="mnd-badge mnd-bad">AUTHORSHIP INVALID</span>';
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
<button class="mnd-btn mnd-btn-danger" data-revoke="${esc(m.id)}">Revoke</button>
|
|
133
|
-
</div>`
|
|
134
|
-
: `<div class="mnd-dead-note">${m.revoked ? `revoked ${fmtWhen(m.revoked.at)} — ${esc(m.revoked.reason)}` : `expired ${fmtWhen(m.expiresAt)}`}</div>`;
|
|
194
|
+
// Internals (id, agent fingerprints, scope slug, raw action names, bounds)
|
|
195
|
+
// are SUPPORT metadata, never the headline — kept on one muted line so the
|
|
196
|
+
// operator can quote an id when asking for help, but never has to read JSON.
|
|
197
|
+
const actionSlugs = (m.authorities || []).map((a) => esc(a.action)).filter(Boolean).join(', ') || 'none';
|
|
198
|
+
const referenceLine = `<div class="mnd-meta">For support: id <code>${esc(m.id)}</code> · agents <code>${esc(shortFp(m.agents?.[0]))}</code> + <code>${esc(shortFp(m.agents?.[1]))}</code> · scope <code>${esc(m.scope)}</code> · authorizes ${actionSlugs} · issued by ${esc(m.author)}</div>`;
|
|
199
|
+
// The grant form is the PRIMARY action and renders ABOVE revoke. Revoke is
|
|
200
|
+
// destructive, so it is demoted to a quiet collapsed control, never featured.
|
|
135
201
|
const grantUi = state === 'active' ? renderGrantForm(m, slackUsers) : '';
|
|
202
|
+
const revokeUi = state === 'active'
|
|
203
|
+
? `<details class="mnd-revoke-details">
|
|
204
|
+
<summary class="mnd-revoke-summary">Revoke this permission slip</summary>
|
|
205
|
+
<div class="mnd-revoke-row">
|
|
206
|
+
<input type="password" class="mnd-pin" data-revoke-pin="${esc(m.id)}" placeholder="Your PIN" autocomplete="off" />
|
|
207
|
+
<input type="text" class="mnd-reason" data-revoke-reason="${esc(m.id)}" placeholder="reason (optional)" />
|
|
208
|
+
<button class="mnd-btn mnd-btn-danger" data-revoke="${esc(m.id)}">Revoke</button>
|
|
209
|
+
</div>
|
|
210
|
+
<span class="mnd-hint">Ends the slip and everything it authorized, right away. This cannot be undone.</span>
|
|
211
|
+
</details>`
|
|
212
|
+
: `<div class="mnd-dead-note">${m.revoked ? `Revoked ${fmtWhen(m.revoked.at)} — ${esc(m.revoked.reason)}` : `Expired ${fmtWhen(m.expiresAt)}`}</div>`;
|
|
136
213
|
return `<div class="mnd-card">
|
|
137
214
|
<div class="mnd-card-head">
|
|
138
|
-
<span class="mnd-scope">${esc(m.scope)}</span>
|
|
139
|
-
<span class="mnd-badge ${stateCls}">${
|
|
215
|
+
<span class="mnd-scope">${esc(humanScope(m.scope))}</span>
|
|
216
|
+
<span class="mnd-badge ${stateCls}">${stateLabel}</span>
|
|
140
217
|
${authBadge}
|
|
141
218
|
</div>
|
|
142
|
-
<
|
|
143
|
-
<ul class="mnd-authorities">${authorities}</ul>
|
|
219
|
+
<p class="mnd-summary">${humanSummary(m)}</p>
|
|
144
220
|
${renderGrants(m, slackUsers)}
|
|
145
221
|
${grantUi}
|
|
222
|
+
${referenceLine}
|
|
146
223
|
${revokeUi}
|
|
147
224
|
</div>`;
|
|
148
225
|
}).join('');
|
|
@@ -157,13 +234,17 @@ export function renderAudit(payload) {
|
|
|
157
234
|
if (entries.length === 0) {
|
|
158
235
|
return `<div class="mnd-audit-head">${chainBadge}</div><div class="mnd-empty">No decisions recorded yet.</div>`;
|
|
159
236
|
}
|
|
237
|
+
// Mobile-first (Operator-Surface Quality item 5): each cell carries a
|
|
238
|
+
// data-label so the table can stack into labelled rows at phone width (the
|
|
239
|
+
// CSS media query in index.html), instead of cramming five columns and
|
|
240
|
+
// truncating the most useful one — the reason — into an unreadable sliver.
|
|
160
241
|
const rows = entries.slice(-25).reverse().map((e) =>
|
|
161
242
|
`<tr>
|
|
162
|
-
<td>${fmtWhen(e.ts)}</td>
|
|
163
|
-
<td><span class="mnd-badge ${e.decision === 'allow' ? 'mnd-ok' : 'mnd-deny'}">${esc(e.decision)}</span></td>
|
|
164
|
-
<td><code>${esc(e.action)}</code></td>
|
|
165
|
-
<td><code>${esc(shortFp(e.agentFp))}</code></td>
|
|
166
|
-
<td class="mnd-reason-cell">${esc(e.reason)}</td>
|
|
243
|
+
<td data-label="When">${fmtWhen(e.ts)}</td>
|
|
244
|
+
<td data-label="Decision"><span class="mnd-badge ${e.decision === 'allow' ? 'mnd-ok' : 'mnd-deny'}">${esc(e.decision)}</span></td>
|
|
245
|
+
<td data-label="Action"><code>${esc(e.action)}</code></td>
|
|
246
|
+
<td data-label="Agent"><code>${esc(shortFp(e.agentFp))}</code></td>
|
|
247
|
+
<td data-label="Reason" class="mnd-reason-cell">${esc(e.reason)}</td>
|
|
167
248
|
</tr>`,
|
|
168
249
|
).join('');
|
|
169
250
|
return `<div class="mnd-audit-head">${chainBadge}<span class="mnd-dim">${entries.length} total decisions · newest first</span></div>
|
|
@@ -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;
|
|
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;AA8G7D,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;AAm1CD,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,CA6yZtE;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
|
@@ -85,6 +85,8 @@ import { GitSyncManager } from '../core/GitSync.js';
|
|
|
85
85
|
import { RegistrySyncDebouncer } from '../core/RegistrySyncDebouncer.js';
|
|
86
86
|
import { wireRegistrySync } from '../core/wireRegistrySync.js';
|
|
87
87
|
import { assertSeamlessnessInvariants } from '../core/seamlessnessConfig.js';
|
|
88
|
+
import { assertStateSyncInvariants } from '../core/stateSyncConfig.js';
|
|
89
|
+
import { ReplicatedKindRegistry, checkPoolFlagCoherence } from '../core/ReplicatedRecordEnvelope.js';
|
|
88
90
|
import { FencedLease } from '../core/FencedLease.js';
|
|
89
91
|
import { GitLeaseStore } from '../core/GitLeaseStore.js';
|
|
90
92
|
import { LocalLeaseStore } from '../core/LocalLeaseStore.js';
|
|
@@ -3271,6 +3273,38 @@ export async function startServer(options) {
|
|
|
3271
3273
|
// that breaks the RPO bound) is REJECTED here with a clear message rather
|
|
3272
3274
|
// than degrading silently. Default/absent config resolves to valid values.
|
|
3273
3275
|
const seamlessness = assertSeamlessnessInvariants(config.multiMachine);
|
|
3276
|
+
// Replicated-store foundation (multi-machine-replicated-store-foundation §10.2)
|
|
3277
|
+
// — resolve + validate the FOUNDATION-LEVEL stateSync knobs at startup. An
|
|
3278
|
+
// out-of-range value (e.g. a maxDriftMs below the 60s floor that would start
|
|
3279
|
+
// quarantining ordinary NTP jitter, or a non-positive journal budget) is
|
|
3280
|
+
// REJECTED here with a clear message rather than silently coerced (§10.2:
|
|
3281
|
+
// "a bad config is REJECTED, not silently degraded"). Default/absent config
|
|
3282
|
+
// resolves to valid values. The per-store on-switches are dark by default and
|
|
3283
|
+
// not range-validated; with none on, the foundation is inert.
|
|
3284
|
+
const stateSync = assertStateSyncInvariants(config.multiMachine);
|
|
3285
|
+
void stateSync; // foundation knobs are consumed by the store PRs (WS2.1+)
|
|
3286
|
+
// The replicated-kind registry (Component 2). Ships EMPTY in this step — the
|
|
3287
|
+
// first concrete store (WS2.1) registers its kind onto it. Constructed here so
|
|
3288
|
+
// the substrate is wired and the per-store stateSyncReceive advert can be
|
|
3289
|
+
// self-reported from the set of registered+enabled stores (machinery presence,
|
|
3290
|
+
// never a hardcoded true). A machine advertises a store's receive capability
|
|
3291
|
+
// ONLY when that store's kind is registered here AND emission is enabled in
|
|
3292
|
+
// config — i.e. it can both validate AND apply that kind. With an empty
|
|
3293
|
+
// registry the advert is `{}` (non-participant for every store): the correct
|
|
3294
|
+
// single-machine / no-store-yet behavior (a strict no-op).
|
|
3295
|
+
const replicatedKindRegistry = new ReplicatedKindRegistry();
|
|
3296
|
+
const selfStateSyncReceive = () => {
|
|
3297
|
+
const out = {};
|
|
3298
|
+
const stores = config.multiMachine?.stateSync;
|
|
3299
|
+
for (const store of replicatedKindRegistry.stores()) {
|
|
3300
|
+
// Advertise the receive capability iff the machinery exists (kind
|
|
3301
|
+
// registered) AND the store is enabled here — so a peer never forwards a
|
|
3302
|
+
// kind we'd silently drop (the named skew-failure mode).
|
|
3303
|
+
if (stores?.[store]?.enabled === true)
|
|
3304
|
+
out[store] = true;
|
|
3305
|
+
}
|
|
3306
|
+
return out;
|
|
3307
|
+
};
|
|
3274
3308
|
// Read local signing key for machine route authentication
|
|
3275
3309
|
let localSigningKeyPem = '';
|
|
3276
3310
|
if (coordinator.enabled && coordinator.identity) {
|
|
@@ -12150,7 +12184,7 @@ export async function startServer(options) {
|
|
|
12150
12184
|
// WS1.1 capability advertisement (spec invariant 5): a bounded
|
|
12151
12185
|
// fixed-size summary, never an inventory. Reported live each
|
|
12152
12186
|
// heartbeat so a queue going dark withdraws the capability.
|
|
12153
|
-
seamlessnessFlags: { ws11DeliverReceive: !!_inboundQueue, ws12DrainReceive: !!_drainRunner, ws44PoolLinks: !!_poolLink },
|
|
12187
|
+
seamlessnessFlags: { ws11DeliverReceive: !!_inboundQueue, ws12DrainReceive: !!_drainRunner, ws44PoolLinks: !!_poolLink, stateSyncReceive: selfStateSyncReceive() },
|
|
12154
12188
|
// Durable Inbound Message Queue §5.1: depth + oldest + tenure +
|
|
12155
12189
|
// bounded top-K — the survivor's loss-SUSPECTED item, capped
|
|
12156
12190
|
// re-placement arm, and supersede-dedupe key all read these.
|
|
@@ -12189,6 +12223,46 @@ export async function startServer(options) {
|
|
|
12189
12223
|
const poolTimer = setInterval(refreshPool, 30_000);
|
|
12190
12224
|
if (typeof poolTimer.unref === 'function')
|
|
12191
12225
|
poolTimer.unref();
|
|
12226
|
+
// Boot-time pool-flag-coherence check (multi-machine-replicated-store-
|
|
12227
|
+
// foundation §4). For each LOCALLY-ENABLED replicated store, surface
|
|
12228
|
+
// ONCE (coalesced — never per-peer-per-tick) any peer that does NOT
|
|
12229
|
+
// advertise the matching stateSyncReceive capability: that peer would
|
|
12230
|
+
// SILENTLY DROP our kind (the journal applier drops unknown kinds), the
|
|
12231
|
+
// NAMED data-loss skew mode. Correct for N peers — checkPoolFlagCoherence
|
|
12232
|
+
// iterates ALL advertising peers. With an EMPTY registry (the Step-2
|
|
12233
|
+
// substrate-only state) this is a strict no-op (no registered stores →
|
|
12234
|
+
// empty verdict → nothing surfaced). The first concrete store (WS2.1)
|
|
12235
|
+
// registers its kind and this check starts doing real work automatically.
|
|
12236
|
+
let stateSyncCoherenceSurfaced = false;
|
|
12237
|
+
const checkStateSyncCoherence = () => {
|
|
12238
|
+
if (stateSyncCoherenceSurfaced)
|
|
12239
|
+
return;
|
|
12240
|
+
if (replicatedKindRegistry.size === 0)
|
|
12241
|
+
return; // no concrete store yet
|
|
12242
|
+
const peers = machinePoolRegistry
|
|
12243
|
+
.getCapacities()
|
|
12244
|
+
.filter((c) => c.machineId !== poolSelfId)
|
|
12245
|
+
.map((c) => ({
|
|
12246
|
+
machineId: c.machineId,
|
|
12247
|
+
online: c.online,
|
|
12248
|
+
stateSyncReceive: c.seamlessnessFlags?.stateSyncReceive,
|
|
12249
|
+
}));
|
|
12250
|
+
const stores = config.multiMachine?.stateSync;
|
|
12251
|
+
const verdict = checkPoolFlagCoherence(replicatedKindRegistry, stores, peers);
|
|
12252
|
+
if (verdict.mixedStores.length > 0) {
|
|
12253
|
+
// Surface ONCE (coalesced): one log line listing every mixed store.
|
|
12254
|
+
// A richer surface (one Attention item) is the store PR's to add;
|
|
12255
|
+
// the substrate guarantees the single, deduped detection here.
|
|
12256
|
+
stateSyncCoherenceSurfaced = true;
|
|
12257
|
+
console.log(pc.yellow(` [stateSync] pool-flag-coherence — mixed flag state across ${verdict.mixedStores.length} store(s):`));
|
|
12258
|
+
for (const line of verdict.summary)
|
|
12259
|
+
console.log(pc.yellow(` • ${line}`));
|
|
12260
|
+
}
|
|
12261
|
+
};
|
|
12262
|
+
checkStateSyncCoherence();
|
|
12263
|
+
const coherenceTimer = setInterval(checkStateSyncCoherence, 60_000);
|
|
12264
|
+
if (typeof coherenceTimer.unref === 'function')
|
|
12265
|
+
coherenceTimer.unref();
|
|
12192
12266
|
}
|
|
12193
12267
|
}
|
|
12194
12268
|
}
|