@etiquekit/etq 1.0.14 → 1.0.15
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/AGENTS.md +26 -19
- package/LICENSE +3 -2
- package/NOTICE +4 -6
- package/QuickStart.md +46 -35
- package/README.md +56 -56
- package/bin/etiquette +5 -1
- package/bin/etiquette-core +5 -1
- package/docs/ARCHITECTURE.md +17 -20
- package/docs/CONCEPTS.md +5 -5
- package/docs/CORE_PROFILE.md +15 -7
- package/docs/LANE_PROVISIONING.md +2 -7
- package/docs/README.md +14 -13
- package/docs/RELEASE_SURFACE_AUDIT.md +7 -8
- package/docs/SEAT_DISCIPLINE.md +91 -55
- package/docs/SEAT_PROVISIONING.md +19 -16
- package/docs/TEAM_HANDOFF.md +21 -21
- package/docs/WORKTREE_QOL.md +0 -7
- package/docs/contracts/ledger-entry/README.md +11 -11
- package/docs/contracts/ledger-entry/ledger-entry.v0.2.md +2 -2
- package/docs/contracts/ledger-entry/ledger-entry.v0.md +8 -8
- package/lib/etiquette-core.js +313 -0
- package/lib/etiquette.js +1151 -0
- package/package.json +11 -10
- package/templates/etiquette-vanilla-v0/README.md +3 -2
- package/templates/etiquette-vanilla-v0/source/control-seat/README.md +6 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/access-assurance-check +55 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/bin/seat-doctor +5 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/ACCESS-ASSURANCE.md +39 -0
- package/templates/etiquette-vanilla-v0/source/control-seat/docs/work/access/workspace-secure-profile.v0.json +53 -0
- package/templates/etiquette-vanilla-v0/validate-vanilla.sh +5 -0
- package/templates/hosted-receiver/README.md +41 -0
- package/templates/hosted-receiver/w1-github-webhook-receiver.mjs +129 -0
- package/templates/seat-packs-v0/README.md +2 -3
- package/templates/seat-packs-v0/source/claude-code-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/codex-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/gemini-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/ollama-seat/README.md +3 -3
- package/templates/seat-packs-v0/source/openrouter-seat/README.md +3 -3
- package/docs/CONCEPT_STATUS.md +0 -66
- package/packages/control/src/authority/lease.ts +0 -261
- package/packages/control/src/authority/node-delegation.ts +0 -257
- package/packages/control/src/authority/rig-conductor.ts +0 -632
- package/packages/control/src/cli/argv.ts +0 -155
- package/packages/control/src/cli/commands/console.ts +0 -200
- package/packages/control/src/cli/commands/dispatch-core.ts +0 -89
- package/packages/control/src/cli/commands/dispatch.ts +0 -279
- package/packages/control/src/cli/commands/harness.ts +0 -89
- package/packages/control/src/cli/commands/hook.ts +0 -50
- package/packages/control/src/cli/commands/ledger.ts +0 -91
- package/packages/control/src/cli/commands/local-workflow.ts +0 -4690
- package/packages/control/src/cli/commands/memory.ts +0 -445
- package/packages/control/src/cli/commands/release.ts +0 -108
- package/packages/control/src/cli/commands/rubric.ts +0 -103
- package/packages/control/src/cli/commands/seat.ts +0 -179
- package/packages/control/src/cli/commands/session.ts +0 -127
- package/packages/control/src/cli/commands/supervision.ts +0 -246
- package/packages/control/src/cli/commands/sync.ts +0 -119
- package/packages/control/src/cli/commands/workflow.ts +0 -86
- package/packages/control/src/cli/commands/workspace.ts +0 -50
- package/packages/control/src/cli/core-usage.ts +0 -34
- package/packages/control/src/cli/prompt.ts +0 -67
- package/packages/control/src/cli/supervision-deps.ts +0 -44
- package/packages/control/src/cli/usage.ts +0 -241
- package/packages/control/src/cli.ts +0 -207
- package/packages/control/src/core-cli.ts +0 -50
- package/packages/control/src/dispatch/decision.ts +0 -202
- package/packages/control/src/dispatch/projection.ts +0 -293
- package/packages/control/src/dispatch/record.ts +0 -153
- package/packages/control/src/engagement/project.ts +0 -170
- package/packages/control/src/fs.ts +0 -19
- package/packages/control/src/harness/pruning.ts +0 -406
- package/packages/control/src/hooks/dispatcher.ts +0 -117
- package/packages/control/src/hooks/outbox.ts +0 -86
- package/packages/control/src/hooks/sanitize.ts +0 -6
- package/packages/control/src/hooks/types.ts +0 -34
- package/packages/control/src/index.ts +0 -384
- package/packages/control/src/ledger/entry.ts +0 -303
- package/packages/control/src/ledger/indexer.ts +0 -542
- package/packages/control/src/memory/context.ts +0 -149
- package/packages/control/src/memory/drain-import.ts +0 -207
- package/packages/control/src/memory/indexer.ts +0 -284
- package/packages/control/src/memory/query.ts +0 -75
- package/packages/control/src/memory/sanitize.ts +0 -50
- package/packages/control/src/memory/sharded-drain-import.ts +0 -212
- package/packages/control/src/memory/status.ts +0 -211
- package/packages/control/src/memory/store-lifecycle.ts +0 -509
- package/packages/control/src/memory/store.ts +0 -284
- package/packages/control/src/memory/types.ts +0 -146
- package/packages/control/src/parity/surfaces.ts +0 -748
- package/packages/control/src/project.ts +0 -141
- package/packages/control/src/projection/local-ledger-view.ts +0 -373
- package/packages/control/src/projection/return-enforcement.ts +0 -48
- package/packages/control/src/projection/timeline-preview.ts +0 -539
- package/packages/control/src/projection/timeline.ts +0 -708
- package/packages/control/src/release/readiness.ts +0 -842
- package/packages/control/src/rubric/loader.ts +0 -326
- package/packages/control/src/rubric/promotion.ts +0 -54
- package/packages/control/src/rubric/runner.ts +0 -159
- package/packages/control/src/rubric/types.ts +0 -158
- package/packages/control/src/seat/owner-card.ts +0 -388
- package/packages/control/src/seat/readiness.ts +0 -834
- package/packages/control/src/session/runbook.ts +0 -431
- package/packages/control/src/shared/sanitize.ts +0 -49
- package/packages/control/src/supervision/action-classes.ts +0 -192
- package/packages/control/src/supervision/command-apply.ts +0 -378
- package/packages/control/src/supervision/errors.ts +0 -14
- package/packages/control/src/supervision/event-replay.ts +0 -155
- package/packages/control/src/supervision/events.ts +0 -109
- package/packages/control/src/supervision/index.ts +0 -16
- package/packages/control/src/supervision/manifest.ts +0 -127
- package/packages/control/src/supervision/paths.ts +0 -49
- package/packages/control/src/supervision/projection-adapter.ts +0 -274
- package/packages/control/src/supervision/projection.ts +0 -75
- package/packages/control/src/supervision/rebuild.ts +0 -99
- package/packages/control/src/supervision/session-open.ts +0 -131
- package/packages/control/src/supervision/session-read.ts +0 -99
- package/packages/control/src/supervision/sqlite-impl.ts +0 -71
- package/packages/control/src/supervision/sqlite.ts +0 -121
- package/packages/control/src/supervision/store-rows.ts +0 -371
- package/packages/control/src/supervision/turn-close.ts +0 -154
- package/packages/control/src/supervision/turn-open.ts +0 -284
- package/packages/control/src/sync/event-log-merge-driver.ts +0 -263
- package/packages/control/src/sync/join-plan.ts +0 -375
- package/packages/control/src/sync/outbox.ts +0 -492
- package/packages/control/src/workflow/evaluator.ts +0 -140
- package/packages/control/src/workflow/loader.ts +0 -200
- package/packages/control/src/workflow/types.ts +0 -90
- package/packages/control/src/workspace/authority.ts +0 -499
- package/packages/protocol/src/guards.ts +0 -119
- package/packages/protocol/src/huddle-board.ts +0 -198
- package/packages/protocol/src/huddle.ts +0 -295
- package/packages/protocol/src/incident.ts +0 -251
- package/packages/protocol/src/index.ts +0 -8
- package/packages/protocol/src/interfaces.ts +0 -107
- package/packages/protocol/src/packet-profile.ts +0 -195
- package/packages/protocol/src/state.ts +0 -81
- package/packages/protocol/src/types.ts +0 -434
- package/release/lineage.v0.json +0 -15
- package/scripts/release-candidate-verify.sh +0 -175
- package/scripts/release-checksum.sh +0 -25
- package/scripts/release-pack-canary.sh +0 -97
- package/scripts/release-scan.sh +0 -249
- package/scripts/release-sign.sh +0 -34
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
type JsonRecord = Record<string, unknown>;
|
|
2
|
-
|
|
3
|
-
export const AUTHORITY_LEASE_SCHEMA = 'authority_lease.v0';
|
|
4
|
-
export const AUTHORITY_LEASE_ACTION_VALIDATION_SCHEMA = 'authority-lease-action-validation.v0';
|
|
5
|
-
|
|
6
|
-
export interface AuthorityLeasePrincipal {
|
|
7
|
-
principal: string;
|
|
8
|
-
authority_source: 'human_gate' | 'policy_gate' | 'human_or_policy_gate';
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface AuthorityLeaseSubject {
|
|
12
|
-
seat_id: string;
|
|
13
|
-
operator_principal?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export interface AuthorityLeaseScope {
|
|
17
|
-
repo_ref?: string;
|
|
18
|
-
workflow?: string;
|
|
19
|
-
task_classes: string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface AuthorityLeaseValidationEnvelope {
|
|
23
|
-
required: string[];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface AuthorityLeaseAuthorityFlags {
|
|
27
|
-
can_mint_authority: false;
|
|
28
|
-
can_extend_own_lease: false;
|
|
29
|
-
can_subdelegate: false;
|
|
30
|
-
can_auto_merge_in_v0: false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface AuthorityLease {
|
|
34
|
-
schema: typeof AUTHORITY_LEASE_SCHEMA;
|
|
35
|
-
lease_id: string;
|
|
36
|
-
issued_at: string;
|
|
37
|
-
expires_at: string;
|
|
38
|
-
issued_by: AuthorityLeasePrincipal;
|
|
39
|
-
issued_to: AuthorityLeaseSubject;
|
|
40
|
-
scope: AuthorityLeaseScope;
|
|
41
|
-
allowed_tools: string[];
|
|
42
|
-
gated_tools: string[];
|
|
43
|
-
forbidden_tools: string[];
|
|
44
|
-
validation_envelope: AuthorityLeaseValidationEnvelope;
|
|
45
|
-
revocation: {
|
|
46
|
-
canonical_revocation_ref: string | null;
|
|
47
|
-
};
|
|
48
|
-
authority_flags: AuthorityLeaseAuthorityFlags;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface AuthorityLeaseCanonicalState {
|
|
52
|
-
at: string;
|
|
53
|
-
revoked_lease_ids?: string[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface AuthorityLeaseValidationInput {
|
|
57
|
-
capability: string;
|
|
58
|
-
validation_results: Record<string, boolean>;
|
|
59
|
-
canonical_state: AuthorityLeaseCanonicalState;
|
|
60
|
-
evidence_ref?: string;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface AuthorityLeaseActionValidation {
|
|
64
|
-
schema: typeof AUTHORITY_LEASE_ACTION_VALIDATION_SCHEMA;
|
|
65
|
-
lease_ref: string;
|
|
66
|
-
capability_used: string;
|
|
67
|
-
verdict: 'legitimate' | 'blocked';
|
|
68
|
-
reasons: string[];
|
|
69
|
-
validation: {
|
|
70
|
-
required: string[];
|
|
71
|
-
passed: string[];
|
|
72
|
-
missing_or_failed: string[];
|
|
73
|
-
};
|
|
74
|
-
evidence_ref: string | null;
|
|
75
|
-
authority_boundary: AuthorityLeaseAuthorityFlags & {
|
|
76
|
-
minted_authority: false;
|
|
77
|
-
point_of_effect_checked: true;
|
|
78
|
-
telemetry_is_audit_log: false;
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function isRecord(value: unknown): value is JsonRecord {
|
|
83
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function asRecord(value: unknown, field: string): JsonRecord {
|
|
87
|
-
if (!isRecord(value)) throw new Error(`${field} must be an object`);
|
|
88
|
-
return value;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function asString(record: JsonRecord, field: string): string {
|
|
92
|
-
const value = record[field];
|
|
93
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
94
|
-
throw new Error(`${field} must be a non-empty string`);
|
|
95
|
-
}
|
|
96
|
-
return value.trim();
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function optionalString(record: JsonRecord, field: string): string | undefined {
|
|
100
|
-
const value = record[field];
|
|
101
|
-
if (value === undefined) return undefined;
|
|
102
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
103
|
-
throw new Error(`${field} must be a non-empty string when present`);
|
|
104
|
-
}
|
|
105
|
-
return value.trim();
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function asStringArray(record: JsonRecord, field: string): string[] {
|
|
109
|
-
const value = record[field];
|
|
110
|
-
if (!Array.isArray(value) || value.length === 0) throw new Error(`${field} must be a non-empty string array`);
|
|
111
|
-
return value.map((item) => {
|
|
112
|
-
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
113
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
114
|
-
}
|
|
115
|
-
return item.trim();
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function asFalse(record: JsonRecord, field: keyof AuthorityLeaseAuthorityFlags): false {
|
|
120
|
-
if (record[field] !== false) throw new Error(`authority_flags.${field} must be false`);
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function assertIso(value: string, field: string): string {
|
|
125
|
-
const date = new Date(value);
|
|
126
|
-
if (!Number.isFinite(date.getTime())) throw new Error(`${field} must be an ISO-8601 timestamp`);
|
|
127
|
-
return date.toISOString();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function assertPortableRef(value: string, field: string): void {
|
|
131
|
-
if (value.startsWith('/') || value.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(value)) {
|
|
132
|
-
throw new Error(`${field} must be an opaque or repo-relative ref, not a local absolute path`);
|
|
133
|
-
}
|
|
134
|
-
if (/[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
|
|
135
|
-
throw new Error(`${field} must be an opaque or repo-relative ref, not a URL`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function assertNonOverlapping(left: string[], right: string[], leftName: string, rightName: string): void {
|
|
140
|
-
const overlap = left.filter((item) => right.includes(item));
|
|
141
|
-
if (overlap.length > 0) throw new Error(`${leftName} and ${rightName} overlap: ${overlap.join(', ')}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function parsePrincipal(value: unknown, field: string): AuthorityLeasePrincipal {
|
|
145
|
-
const record = asRecord(value, field);
|
|
146
|
-
const source = asString(record, 'authority_source');
|
|
147
|
-
if (!['human_gate', 'policy_gate', 'human_or_policy_gate'].includes(source)) {
|
|
148
|
-
throw new Error('authority_source must be one of: human_gate, policy_gate, human_or_policy_gate');
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
principal: asString(record, 'principal'),
|
|
152
|
-
authority_source: source as AuthorityLeasePrincipal['authority_source'],
|
|
153
|
-
};
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function parseSubject(value: unknown): AuthorityLeaseSubject {
|
|
157
|
-
const record = asRecord(value, 'issued_to');
|
|
158
|
-
return {
|
|
159
|
-
seat_id: asString(record, 'seat_id'),
|
|
160
|
-
operator_principal: optionalString(record, 'operator_principal'),
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function parseScope(value: unknown): AuthorityLeaseScope {
|
|
165
|
-
const record = asRecord(value, 'scope');
|
|
166
|
-
const repoRef = optionalString(record, 'repo_ref');
|
|
167
|
-
if (repoRef) assertPortableRef(repoRef, 'scope.repo_ref');
|
|
168
|
-
return {
|
|
169
|
-
repo_ref: repoRef,
|
|
170
|
-
workflow: optionalString(record, 'workflow'),
|
|
171
|
-
task_classes: asStringArray(record, 'task_classes'),
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function parseAuthorityFlags(value: unknown): AuthorityLeaseAuthorityFlags {
|
|
176
|
-
const record = asRecord(value, 'authority_flags');
|
|
177
|
-
return {
|
|
178
|
-
can_mint_authority: asFalse(record, 'can_mint_authority'),
|
|
179
|
-
can_extend_own_lease: asFalse(record, 'can_extend_own_lease'),
|
|
180
|
-
can_subdelegate: asFalse(record, 'can_subdelegate'),
|
|
181
|
-
can_auto_merge_in_v0: asFalse(record, 'can_auto_merge_in_v0'),
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export function parseAuthorityLease(value: unknown): AuthorityLease {
|
|
186
|
-
const record = asRecord(value, 'authority lease');
|
|
187
|
-
if (record.schema !== AUTHORITY_LEASE_SCHEMA) throw new Error(`schema must be ${AUTHORITY_LEASE_SCHEMA}`);
|
|
188
|
-
const issuedAt = assertIso(asString(record, 'issued_at'), 'issued_at');
|
|
189
|
-
const expiresAt = assertIso(asString(record, 'expires_at'), 'expires_at');
|
|
190
|
-
if (Date.parse(expiresAt) <= Date.parse(issuedAt)) throw new Error('expires_at must be after issued_at');
|
|
191
|
-
const allowedTools = asStringArray(record, 'allowed_tools');
|
|
192
|
-
const gatedTools = asStringArray(record, 'gated_tools');
|
|
193
|
-
const forbiddenTools = asStringArray(record, 'forbidden_tools');
|
|
194
|
-
assertNonOverlapping(allowedTools, forbiddenTools, 'allowed_tools', 'forbidden_tools');
|
|
195
|
-
assertNonOverlapping(gatedTools, forbiddenTools, 'gated_tools', 'forbidden_tools');
|
|
196
|
-
const validation = asRecord(record.validation_envelope, 'validation_envelope');
|
|
197
|
-
const revocation = asRecord(record.revocation, 'revocation');
|
|
198
|
-
const canonicalRevocationRef = revocation.canonical_revocation_ref;
|
|
199
|
-
if (canonicalRevocationRef !== null && typeof canonicalRevocationRef !== 'string') {
|
|
200
|
-
throw new Error('revocation.canonical_revocation_ref must be a string or null');
|
|
201
|
-
}
|
|
202
|
-
if (typeof canonicalRevocationRef === 'string') assertPortableRef(canonicalRevocationRef, 'revocation.canonical_revocation_ref');
|
|
203
|
-
return {
|
|
204
|
-
schema: AUTHORITY_LEASE_SCHEMA,
|
|
205
|
-
lease_id: asString(record, 'lease_id'),
|
|
206
|
-
issued_at: issuedAt,
|
|
207
|
-
expires_at: expiresAt,
|
|
208
|
-
issued_by: parsePrincipal(record.issued_by, 'issued_by'),
|
|
209
|
-
issued_to: parseSubject(record.issued_to),
|
|
210
|
-
scope: parseScope(record.scope),
|
|
211
|
-
allowed_tools: allowedTools,
|
|
212
|
-
gated_tools: gatedTools,
|
|
213
|
-
forbidden_tools: forbiddenTools,
|
|
214
|
-
validation_envelope: {
|
|
215
|
-
required: asStringArray(validation, 'required'),
|
|
216
|
-
},
|
|
217
|
-
revocation: {
|
|
218
|
-
canonical_revocation_ref: canonicalRevocationRef,
|
|
219
|
-
},
|
|
220
|
-
authority_flags: parseAuthorityFlags(record.authority_flags),
|
|
221
|
-
};
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function evaluateAuthorityLeaseAction(
|
|
225
|
-
rawLease: unknown,
|
|
226
|
-
input: AuthorityLeaseValidationInput,
|
|
227
|
-
): AuthorityLeaseActionValidation {
|
|
228
|
-
const lease = parseAuthorityLease(rawLease);
|
|
229
|
-
const reasons: string[] = [];
|
|
230
|
-
const pointInTime = Date.parse(assertIso(input.canonical_state.at, 'canonical_state.at'));
|
|
231
|
-
if (pointInTime > Date.parse(lease.expires_at)) reasons.push('lease_expired');
|
|
232
|
-
if (lease.revocation.canonical_revocation_ref) reasons.push('lease_revoked');
|
|
233
|
-
if ((input.canonical_state.revoked_lease_ids ?? []).includes(lease.lease_id)) {
|
|
234
|
-
reasons.push('lease_revoked_at_point_of_effect');
|
|
235
|
-
}
|
|
236
|
-
if (lease.forbidden_tools.includes(input.capability)) reasons.push('capability_forbidden');
|
|
237
|
-
if (lease.gated_tools.includes(input.capability)) reasons.push('capability_requires_external_gate');
|
|
238
|
-
if (!lease.allowed_tools.includes(input.capability)) reasons.push('capability_not_allowed');
|
|
239
|
-
const missingOrFailed = lease.validation_envelope.required.filter((item) => input.validation_results[item] !== true);
|
|
240
|
-
if (missingOrFailed.length > 0) reasons.push('validation_envelope_failed');
|
|
241
|
-
const passed = lease.validation_envelope.required.filter((item) => input.validation_results[item] === true);
|
|
242
|
-
return {
|
|
243
|
-
schema: AUTHORITY_LEASE_ACTION_VALIDATION_SCHEMA,
|
|
244
|
-
lease_ref: lease.lease_id,
|
|
245
|
-
capability_used: input.capability,
|
|
246
|
-
verdict: reasons.length === 0 ? 'legitimate' : 'blocked',
|
|
247
|
-
reasons,
|
|
248
|
-
validation: {
|
|
249
|
-
required: lease.validation_envelope.required,
|
|
250
|
-
passed,
|
|
251
|
-
missing_or_failed: missingOrFailed,
|
|
252
|
-
},
|
|
253
|
-
evidence_ref: input.evidence_ref ?? null,
|
|
254
|
-
authority_boundary: {
|
|
255
|
-
...lease.authority_flags,
|
|
256
|
-
minted_authority: false,
|
|
257
|
-
point_of_effect_checked: true,
|
|
258
|
-
telemetry_is_audit_log: false,
|
|
259
|
-
},
|
|
260
|
-
};
|
|
261
|
-
}
|
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
type JsonRecord = Record<string, unknown>;
|
|
2
|
-
|
|
3
|
-
export const NODE_DELEGATION_SCHEMA = 'node_delegation.v0';
|
|
4
|
-
export const NODE_DELEGATION_VALIDATION_SCHEMA = 'node-delegation-validation.v0';
|
|
5
|
-
|
|
6
|
-
const AUTHORITY_SCOPES = ['none', 'read_only', 'bounded_write', 'promotion'] as const;
|
|
7
|
-
const LEASE_SCOPES = ['none', 'mechanical_validation', 'read_only_review', 'bounded_write', 'bounded_impl'] as const;
|
|
8
|
-
|
|
9
|
-
export type NodeDelegationAuthorityScope = (typeof AUTHORITY_SCOPES)[number];
|
|
10
|
-
export type NodeDelegationLeaseScope = (typeof LEASE_SCOPES)[number];
|
|
11
|
-
|
|
12
|
-
export interface NodeDelegationBoundary {
|
|
13
|
-
parenthood_grants_authority: false;
|
|
14
|
-
child_mints_authority: false;
|
|
15
|
-
child_can_widen_scope: false;
|
|
16
|
-
child_can_write_parent_ledger: false;
|
|
17
|
-
validation_is_authorization: false;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface NodeDelegationLease {
|
|
21
|
-
schema: typeof NODE_DELEGATION_SCHEMA;
|
|
22
|
-
delegation_id: string;
|
|
23
|
-
issued_at: string;
|
|
24
|
-
expires_at: string;
|
|
25
|
-
workflow_id: string;
|
|
26
|
-
lane: string;
|
|
27
|
-
parent_node: string;
|
|
28
|
-
child_node: string;
|
|
29
|
-
return_to: string;
|
|
30
|
-
lease_scope: NodeDelegationLeaseScope;
|
|
31
|
-
authority_scope: NodeDelegationAuthorityScope;
|
|
32
|
-
allowed_writes: string[];
|
|
33
|
-
allowed_tools: string[];
|
|
34
|
-
forbidden_actions: string[];
|
|
35
|
-
may_spawn_children: boolean;
|
|
36
|
-
child_spawn_budget: number;
|
|
37
|
-
max_depth: number;
|
|
38
|
-
requires_preflight: true;
|
|
39
|
-
preflight_required: string[];
|
|
40
|
-
authority_boundary: NodeDelegationBoundary;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface NodeDelegationValidation {
|
|
44
|
-
schema: typeof NODE_DELEGATION_VALIDATION_SCHEMA;
|
|
45
|
-
parent_ref: string;
|
|
46
|
-
child_ref: string;
|
|
47
|
-
verdict: 'legitimate' | 'blocked';
|
|
48
|
-
reasons: string[];
|
|
49
|
-
subset_checks: {
|
|
50
|
-
workflow_matches: boolean;
|
|
51
|
-
lane_matches: boolean;
|
|
52
|
-
parent_chain_matches: boolean;
|
|
53
|
-
child_ttl_within_parent: boolean;
|
|
54
|
-
lease_scope_subset: boolean;
|
|
55
|
-
authority_scope_subset: boolean;
|
|
56
|
-
allowed_writes_subset: boolean;
|
|
57
|
-
allowed_tools_subset: boolean;
|
|
58
|
-
forbidden_actions_inherited: boolean;
|
|
59
|
-
child_spawn_budget_subset: boolean;
|
|
60
|
-
child_depth_reduced: boolean;
|
|
61
|
-
preflight_required: boolean;
|
|
62
|
-
};
|
|
63
|
-
authority_boundary: NodeDelegationBoundary & {
|
|
64
|
-
validation_checked_subset: true;
|
|
65
|
-
validation_grants_authority: false;
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function isRecord(value: unknown): value is JsonRecord {
|
|
70
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function asRecord(value: unknown, field: string): JsonRecord {
|
|
74
|
-
if (!isRecord(value)) throw new Error(`${field} must be an object`);
|
|
75
|
-
return value;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function asString(record: JsonRecord, field: string): string {
|
|
79
|
-
const value = record[field];
|
|
80
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
81
|
-
throw new Error(`${field} must be a non-empty string`);
|
|
82
|
-
}
|
|
83
|
-
return value.trim();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function asStringArray(record: JsonRecord, field: string): string[] {
|
|
87
|
-
const value = record[field];
|
|
88
|
-
if (!Array.isArray(value)) throw new Error(`${field} must be a string array`);
|
|
89
|
-
return value.map((item) => {
|
|
90
|
-
if (typeof item !== 'string' || item.trim().length === 0) {
|
|
91
|
-
throw new Error(`${field} must contain only non-empty strings`);
|
|
92
|
-
}
|
|
93
|
-
return item.trim();
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function asBoolean(record: JsonRecord, field: string): boolean {
|
|
98
|
-
const value = record[field];
|
|
99
|
-
if (typeof value !== 'boolean') throw new Error(`${field} must be boolean`);
|
|
100
|
-
return value;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function asTrue(record: JsonRecord, field: string): true {
|
|
104
|
-
if (record[field] !== true) throw new Error(`${field} must be true`);
|
|
105
|
-
return true;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function asFalse(record: JsonRecord, field: keyof NodeDelegationBoundary): false {
|
|
109
|
-
if (record[field] !== false) throw new Error(`authority_boundary.${field} must be false`);
|
|
110
|
-
return false;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function asNonNegativeInteger(record: JsonRecord, field: string): number {
|
|
114
|
-
const value = record[field];
|
|
115
|
-
if (!Number.isInteger(value) || Number(value) < 0) throw new Error(`${field} must be a non-negative integer`);
|
|
116
|
-
return Number(value);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function assertIso(value: string, field: string): string {
|
|
120
|
-
const date = new Date(value);
|
|
121
|
-
if (!Number.isFinite(date.getTime())) throw new Error(`${field} must be an ISO-8601 timestamp`);
|
|
122
|
-
return date.toISOString();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function assertPortableRef(value: string, field: string): void {
|
|
126
|
-
if (value.startsWith('/') || value.startsWith('\\') || /^[A-Za-z]:[\\/]/.test(value)) {
|
|
127
|
-
throw new Error(`${field} must be an opaque or repo-relative ref, not a local absolute path`);
|
|
128
|
-
}
|
|
129
|
-
if (/[a-z][a-z0-9+.-]*:\/\//i.test(value)) {
|
|
130
|
-
throw new Error(`${field} must be an opaque or repo-relative ref, not a URL`);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function asEnum<T extends readonly string[]>(values: T, value: unknown, field: string): T[number] {
|
|
135
|
-
if (typeof value !== 'string' || !values.includes(value)) {
|
|
136
|
-
throw new Error(`${field} must be one of: ${values.join(', ')}`);
|
|
137
|
-
}
|
|
138
|
-
return value as T[number];
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function parseBoundary(value: unknown): NodeDelegationBoundary {
|
|
142
|
-
const record = asRecord(value, 'authority_boundary');
|
|
143
|
-
return {
|
|
144
|
-
parenthood_grants_authority: asFalse(record, 'parenthood_grants_authority'),
|
|
145
|
-
child_mints_authority: asFalse(record, 'child_mints_authority'),
|
|
146
|
-
child_can_widen_scope: asFalse(record, 'child_can_widen_scope'),
|
|
147
|
-
child_can_write_parent_ledger: asFalse(record, 'child_can_write_parent_ledger'),
|
|
148
|
-
validation_is_authorization: asFalse(record, 'validation_is_authorization'),
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
export function parseNodeDelegationLease(value: unknown): NodeDelegationLease {
|
|
153
|
-
const record = asRecord(value, 'node delegation lease');
|
|
154
|
-
if (record.schema !== NODE_DELEGATION_SCHEMA) throw new Error(`schema must be ${NODE_DELEGATION_SCHEMA}`);
|
|
155
|
-
const issuedAt = assertIso(asString(record, 'issued_at'), 'issued_at');
|
|
156
|
-
const expiresAt = assertIso(asString(record, 'expires_at'), 'expires_at');
|
|
157
|
-
if (Date.parse(expiresAt) <= Date.parse(issuedAt)) throw new Error('expires_at must be after issued_at');
|
|
158
|
-
const delegationId = asString(record, 'delegation_id');
|
|
159
|
-
const lane = asString(record, 'lane');
|
|
160
|
-
const returnTo = asString(record, 'return_to');
|
|
161
|
-
assertPortableRef(delegationId, 'delegation_id');
|
|
162
|
-
assertPortableRef(lane, 'lane');
|
|
163
|
-
assertPortableRef(returnTo, 'return_to');
|
|
164
|
-
const allowedWrites = asStringArray(record, 'allowed_writes');
|
|
165
|
-
for (const ref of allowedWrites) assertPortableRef(ref, 'allowed_writes');
|
|
166
|
-
return {
|
|
167
|
-
schema: NODE_DELEGATION_SCHEMA,
|
|
168
|
-
delegation_id: delegationId,
|
|
169
|
-
issued_at: issuedAt,
|
|
170
|
-
expires_at: expiresAt,
|
|
171
|
-
workflow_id: asString(record, 'workflow_id'),
|
|
172
|
-
lane,
|
|
173
|
-
parent_node: asString(record, 'parent_node'),
|
|
174
|
-
child_node: asString(record, 'child_node'),
|
|
175
|
-
return_to: returnTo,
|
|
176
|
-
lease_scope: asEnum(LEASE_SCOPES, record.lease_scope, 'lease_scope') as NodeDelegationLeaseScope,
|
|
177
|
-
authority_scope: asEnum(AUTHORITY_SCOPES, record.authority_scope, 'authority_scope') as NodeDelegationAuthorityScope,
|
|
178
|
-
allowed_writes: allowedWrites,
|
|
179
|
-
allowed_tools: asStringArray(record, 'allowed_tools'),
|
|
180
|
-
forbidden_actions: asStringArray(record, 'forbidden_actions'),
|
|
181
|
-
may_spawn_children: asBoolean(record, 'may_spawn_children'),
|
|
182
|
-
child_spawn_budget: asNonNegativeInteger(record, 'child_spawn_budget'),
|
|
183
|
-
max_depth: asNonNegativeInteger(record, 'max_depth'),
|
|
184
|
-
requires_preflight: asTrue(record, 'requires_preflight'),
|
|
185
|
-
preflight_required: asStringArray(record, 'preflight_required'),
|
|
186
|
-
authority_boundary: parseBoundary(record.authority_boundary),
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
function rankAuthority(scope: NodeDelegationAuthorityScope): number {
|
|
191
|
-
return AUTHORITY_SCOPES.indexOf(scope);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function rankLease(scope: NodeDelegationLeaseScope): number {
|
|
195
|
-
if (scope === 'none') return 0;
|
|
196
|
-
if (scope === 'mechanical_validation' || scope === 'read_only_review') return 1;
|
|
197
|
-
if (scope === 'bounded_write') return 2;
|
|
198
|
-
return 3;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function isSubset(child: string[], parent: string[]): boolean {
|
|
202
|
-
return child.every((item) => parent.includes(item));
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function includesMinimumPreflight(items: string[]): boolean {
|
|
206
|
-
return ['workflow_binding', 'current_ledger_head', 'lease_envelope'].every((required) => items.includes(required));
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function pushReason(reasons: string[], ok: boolean, reason: string): void {
|
|
210
|
-
if (!ok) reasons.push(reason);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export function evaluateNodeSubdelegation(parentRaw: unknown, childRaw: unknown): NodeDelegationValidation {
|
|
214
|
-
const parent = parseNodeDelegationLease(parentRaw);
|
|
215
|
-
const child = parseNodeDelegationLease(childRaw);
|
|
216
|
-
const checks = {
|
|
217
|
-
workflow_matches: child.workflow_id === parent.workflow_id,
|
|
218
|
-
lane_matches: child.lane === parent.lane,
|
|
219
|
-
parent_chain_matches: child.parent_node === parent.child_node && child.return_to === parent.child_node,
|
|
220
|
-
child_ttl_within_parent: Date.parse(child.expires_at) <= Date.parse(parent.expires_at),
|
|
221
|
-
lease_scope_subset: rankLease(child.lease_scope) <= rankLease(parent.lease_scope),
|
|
222
|
-
authority_scope_subset: rankAuthority(child.authority_scope) <= rankAuthority(parent.authority_scope),
|
|
223
|
-
allowed_writes_subset: isSubset(child.allowed_writes, parent.allowed_writes),
|
|
224
|
-
allowed_tools_subset: isSubset(child.allowed_tools, parent.allowed_tools),
|
|
225
|
-
forbidden_actions_inherited: isSubset(parent.forbidden_actions, child.forbidden_actions),
|
|
226
|
-
child_spawn_budget_subset: parent.may_spawn_children && parent.child_spawn_budget > 0 && child.child_spawn_budget <= parent.child_spawn_budget,
|
|
227
|
-
child_depth_reduced: parent.may_spawn_children && parent.max_depth > 0 && child.max_depth < parent.max_depth,
|
|
228
|
-
preflight_required: child.requires_preflight && includesMinimumPreflight(child.preflight_required),
|
|
229
|
-
};
|
|
230
|
-
const reasons: string[] = [];
|
|
231
|
-
pushReason(reasons, parent.may_spawn_children, 'parent_forbids_children');
|
|
232
|
-
pushReason(reasons, checks.workflow_matches, 'workflow_mismatch');
|
|
233
|
-
pushReason(reasons, checks.lane_matches, 'lane_mismatch');
|
|
234
|
-
pushReason(reasons, checks.parent_chain_matches, 'parent_chain_mismatch');
|
|
235
|
-
pushReason(reasons, checks.child_ttl_within_parent, 'child_ttl_exceeds_parent');
|
|
236
|
-
pushReason(reasons, checks.lease_scope_subset, 'lease_scope_widened');
|
|
237
|
-
pushReason(reasons, checks.authority_scope_subset, 'authority_scope_widened');
|
|
238
|
-
pushReason(reasons, checks.allowed_writes_subset, 'allowed_writes_widened');
|
|
239
|
-
pushReason(reasons, checks.allowed_tools_subset, 'allowed_tools_widened');
|
|
240
|
-
pushReason(reasons, checks.forbidden_actions_inherited, 'forbidden_actions_not_inherited');
|
|
241
|
-
pushReason(reasons, checks.child_spawn_budget_subset, 'child_spawn_budget_exceeds_parent');
|
|
242
|
-
pushReason(reasons, checks.child_depth_reduced, 'child_depth_not_reduced');
|
|
243
|
-
pushReason(reasons, checks.preflight_required, 'preflight_missing_minimums');
|
|
244
|
-
return {
|
|
245
|
-
schema: NODE_DELEGATION_VALIDATION_SCHEMA,
|
|
246
|
-
parent_ref: parent.delegation_id,
|
|
247
|
-
child_ref: child.delegation_id,
|
|
248
|
-
verdict: reasons.length === 0 ? 'legitimate' : 'blocked',
|
|
249
|
-
reasons,
|
|
250
|
-
subset_checks: checks,
|
|
251
|
-
authority_boundary: {
|
|
252
|
-
...child.authority_boundary,
|
|
253
|
-
validation_checked_subset: true,
|
|
254
|
-
validation_grants_authority: false,
|
|
255
|
-
},
|
|
256
|
-
};
|
|
257
|
-
}
|