@wrongstack/governance 0.299.0 → 0.301.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capability-grant.d.ts +8 -0
- package/dist/event-store.d.ts +28 -2
- package/dist/index.js +207 -93
- package/dist/project-daemon.js +187 -84
- package/dist/project-service.d.ts +8 -1
- package/dist/service-protocol.d.ts +4 -0
- package/package.json +1 -1
|
@@ -112,6 +112,14 @@ export declare class GovernanceCapabilityGrantRegistry {
|
|
|
112
112
|
sweepExpired(reason?: string): number;
|
|
113
113
|
getGrant(grantId: string): GovernanceCapabilityGrant | null;
|
|
114
114
|
remainingTtlMs(grantId: string): number | null;
|
|
115
|
+
/**
|
|
116
|
+
* Ordering is deliberately byte-wise rather than locale-aware. Both keys are
|
|
117
|
+
* ASCII by construction — `issuedAt` is an ISO-8601 timestamp and `grantId`
|
|
118
|
+
* matches `^[A-Za-z0-9_-]{1,128}$` — so the two orders are identical, but
|
|
119
|
+
* `localeCompare` would make the order depend on the host's default locale.
|
|
120
|
+
* This list backs the `list_capability_grants` cursor, and a cursor is only
|
|
121
|
+
* sound while the order it walks is stable across the requests that walk it.
|
|
122
|
+
*/
|
|
115
123
|
listGrants(): readonly GovernanceCapabilityGrant[];
|
|
116
124
|
private parseToken;
|
|
117
125
|
private describe;
|
package/dist/event-store.d.ts
CHANGED
|
@@ -4,6 +4,20 @@ import type { VerificationRunContract } from './verification-run-contract.js';
|
|
|
4
4
|
import type { RecordWorkspaceSnapshotResult, WorkspaceSnapshotFenceDescriptor } from './workspace-snapshot-fence.js';
|
|
5
5
|
export declare const GOVERNANCE_OBSERVATION_CATEGORIES: readonly ['task_created', 'status_changed', 'plan_updated', 'tool_invoked', 'evidence_candidate', 'verification_reported', 'review_reported', 'completion_claimed', 'failure_reported', 'capability_grant_issued', 'capability_grant_revoked', 'capability_grant_expired', 'capability_grant_rotated', 'daemon_attachment_broker_lifecycle', 'daemon_shutdown_requested'];
|
|
6
6
|
export type GovernanceObservationCategory = (typeof GOVERNANCE_OBSERVATION_CATEGORIES)[number];
|
|
7
|
+
export declare const GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE = 50;
|
|
8
|
+
export declare const GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE = 100;
|
|
9
|
+
export interface ReadGovernanceObservationsPageOptions {
|
|
10
|
+
readonly projectId: string;
|
|
11
|
+
readonly taskId?: string | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Exact set of categories to return. Callers pass the closed enum subset they
|
|
14
|
+
* are entitled to rather than filtering after the fact, so the database never
|
|
15
|
+
* hands back rows that are about to be discarded.
|
|
16
|
+
*/
|
|
17
|
+
readonly categories: readonly GovernanceObservationCategory[];
|
|
18
|
+
readonly afterSequence: number;
|
|
19
|
+
readonly limit: number;
|
|
20
|
+
}
|
|
7
21
|
export interface GovernanceObservation {
|
|
8
22
|
readonly observationId: string;
|
|
9
23
|
readonly projectId: string;
|
|
@@ -51,7 +65,7 @@ export interface GovernanceEventStore {
|
|
|
51
65
|
readEvents(taskId: string): readonly GovernanceEvent[];
|
|
52
66
|
readReceipt(commandId: string): GovernanceCommandReceipt | null;
|
|
53
67
|
appendObservation(observation: GovernanceObservation): AppendGovernanceObservationResult;
|
|
54
|
-
|
|
68
|
+
readObservationsPage(options: ReadGovernanceObservationsPageOptions): readonly StoredGovernanceObservation[];
|
|
55
69
|
readEvidenceCandidateObservations(projectId: string, taskId: string, options: {
|
|
56
70
|
readonly afterSequence: number;
|
|
57
71
|
readonly limit: number;
|
|
@@ -84,7 +98,19 @@ export declare class SqliteGovernanceEventStore implements GovernanceEventStore
|
|
|
84
98
|
readEvents(taskId: string): readonly GovernanceEvent[];
|
|
85
99
|
readReceipt(commandId: string): GovernanceCommandReceipt | null;
|
|
86
100
|
appendObservation(observation: GovernanceObservation): AppendGovernanceObservationResult;
|
|
87
|
-
|
|
101
|
+
/**
|
|
102
|
+
* Paginated, category-filtered observation read.
|
|
103
|
+
*
|
|
104
|
+
* The observations table is append-only — its UPDATE and DELETE triggers say
|
|
105
|
+
* so — and therefore grows without bound for the lifetime of a project. The
|
|
106
|
+
* previous unpaginated read loaded every row a project had ever recorded and
|
|
107
|
+
* left the caller to filter in JavaScript, which cost 148 MB and 400 ms at
|
|
108
|
+
* 100 000 rows and produced a 8.9 MB response: already past the 8 MB IPC
|
|
109
|
+
* frame cap, so the endpoint could not succeed at all past that point. Both
|
|
110
|
+
* the category filter and the page window belong in SQL, matching what
|
|
111
|
+
* `readEvidenceCandidateObservations` already does.
|
|
112
|
+
*/
|
|
113
|
+
readObservationsPage(options: ReadGovernanceObservationsPageOptions): readonly StoredGovernanceObservation[];
|
|
88
114
|
readEvidenceCandidateObservations(projectId: string, taskId: string, options: {
|
|
89
115
|
readonly afterSequence: number;
|
|
90
116
|
readonly limit: number;
|
package/dist/index.js
CHANGED
|
@@ -309,12 +309,22 @@ var GovernanceCapabilityGrantRegistry = class {
|
|
|
309
309
|
const remaining = record.expiresAtMs - this.now();
|
|
310
310
|
return remaining > 0 ? remaining : null;
|
|
311
311
|
}
|
|
312
|
+
/**
|
|
313
|
+
* Ordering is deliberately byte-wise rather than locale-aware. Both keys are
|
|
314
|
+
* ASCII by construction — `issuedAt` is an ISO-8601 timestamp and `grantId`
|
|
315
|
+
* matches `^[A-Za-z0-9_-]{1,128}$` — so the two orders are identical, but
|
|
316
|
+
* `localeCompare` would make the order depend on the host's default locale.
|
|
317
|
+
* This list backs the `list_capability_grants` cursor, and a cursor is only
|
|
318
|
+
* sound while the order it walks is stable across the requests that walk it.
|
|
319
|
+
*/
|
|
312
320
|
listGrants() {
|
|
313
321
|
const now = this.now();
|
|
314
322
|
return Object.freeze(
|
|
315
|
-
[...this.records.values()].map((record) => this.describe(record, now)).sort(
|
|
316
|
-
(left
|
|
317
|
-
|
|
323
|
+
[...this.records.values()].map((record) => this.describe(record, now)).sort((left, right) => {
|
|
324
|
+
if (left.issuedAt !== right.issuedAt) return left.issuedAt < right.issuedAt ? -1 : 1;
|
|
325
|
+
if (left.grantId === right.grantId) return 0;
|
|
326
|
+
return left.grantId < right.grantId ? -1 : 1;
|
|
327
|
+
})
|
|
318
328
|
);
|
|
319
329
|
}
|
|
320
330
|
parseToken(token) {
|
|
@@ -1076,7 +1086,8 @@ var GovernanceProjectClient = class {
|
|
|
1076
1086
|
return new Promise((resolve6, reject3) => {
|
|
1077
1087
|
const socket = net2.createConnection(this.endpoint);
|
|
1078
1088
|
let settled = false;
|
|
1079
|
-
|
|
1089
|
+
const chunks = [];
|
|
1090
|
+
let size = 0;
|
|
1080
1091
|
const finish = (error, response) => {
|
|
1081
1092
|
if (settled) return;
|
|
1082
1093
|
settled = true;
|
|
@@ -1090,15 +1101,21 @@ var GovernanceProjectClient = class {
|
|
|
1090
1101
|
}, this.timeoutMs);
|
|
1091
1102
|
socket.once("connect", () => socket.write(frame));
|
|
1092
1103
|
socket.on("data", (chunk) => {
|
|
1093
|
-
|
|
1094
|
-
|
|
1104
|
+
if (settled) return;
|
|
1105
|
+
const newline = chunk.indexOf(10);
|
|
1106
|
+
const frameBytes = size + (newline < 0 ? chunk.byteLength : newline);
|
|
1107
|
+
if (frameBytes > GOVERNANCE_IPC_MAX_FRAME_BYTES) {
|
|
1095
1108
|
finish(new Error("Governance IPC response exceeded the frame limit."));
|
|
1096
1109
|
return;
|
|
1097
1110
|
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1111
|
+
if (newline < 0) {
|
|
1112
|
+
chunks.push(chunk);
|
|
1113
|
+
size = frameBytes;
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
chunks.push(chunk.subarray(0, newline));
|
|
1100
1117
|
try {
|
|
1101
|
-
const parsed = JSON.parse(
|
|
1118
|
+
const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
1102
1119
|
const response = decodeGovernanceIpcResponse(parsed);
|
|
1103
1120
|
const expectedRequestId = request && typeof request === "object" && "requestId" in request && typeof request.requestId === "string" ? request.requestId : null;
|
|
1104
1121
|
if (expectedRequestId && response.requestId !== expectedRequestId) {
|
|
@@ -1204,7 +1221,9 @@ var GOVERNANCE_CREDENTIAL_LEASE_DEFAULT_RETRY_MS = 1e3;
|
|
|
1204
1221
|
var GOVERNANCE_CREDENTIAL_LEASE_DEFAULT_MAX_ATTEMPTS = 3;
|
|
1205
1222
|
var defaultScheduler = {
|
|
1206
1223
|
schedule(callback, delayMs) {
|
|
1207
|
-
|
|
1224
|
+
const handle = setTimeout(callback, delayMs);
|
|
1225
|
+
handle.unref();
|
|
1226
|
+
return handle;
|
|
1208
1227
|
},
|
|
1209
1228
|
cancel(handle) {
|
|
1210
1229
|
clearTimeout(handle);
|
|
@@ -1438,7 +1457,7 @@ var GovernanceCredentialLeaseController = class {
|
|
|
1438
1457
|
this.timer = this.scheduler.schedule(() => {
|
|
1439
1458
|
this.timer = void 0;
|
|
1440
1459
|
this.nextActionAtMs = void 0;
|
|
1441
|
-
void this.rotateNow();
|
|
1460
|
+
void this.rotateNow().catch(() => void 0);
|
|
1442
1461
|
}, delayMs);
|
|
1443
1462
|
}
|
|
1444
1463
|
clearTimer() {
|
|
@@ -2245,27 +2264,29 @@ function findCycle(steps, edges) {
|
|
|
2245
2264
|
for (const edge of edges) adjacency.get(edge.fromStepId)?.push(edge.toStepId);
|
|
2246
2265
|
const visiting = /* @__PURE__ */ new Set();
|
|
2247
2266
|
const visited = /* @__PURE__ */ new Set();
|
|
2248
|
-
const
|
|
2249
|
-
const visit = (stepId) => {
|
|
2250
|
-
if (visiting.has(stepId)) {
|
|
2251
|
-
const cycleStart = stack.indexOf(stepId);
|
|
2252
|
-
return [...stack.slice(cycleStart), stepId];
|
|
2253
|
-
}
|
|
2254
|
-
if (visited.has(stepId)) return null;
|
|
2255
|
-
visiting.add(stepId);
|
|
2256
|
-
stack.push(stepId);
|
|
2257
|
-
for (const next of adjacency.get(stepId) ?? []) {
|
|
2258
|
-
const cycle = visit(next);
|
|
2259
|
-
if (cycle) return cycle;
|
|
2260
|
-
}
|
|
2261
|
-
stack.pop();
|
|
2262
|
-
visiting.delete(stepId);
|
|
2263
|
-
visited.add(stepId);
|
|
2264
|
-
return null;
|
|
2265
|
-
};
|
|
2267
|
+
const path6 = [];
|
|
2266
2268
|
for (const step of steps) {
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
+
if (visited.has(step.id)) continue;
|
|
2270
|
+
visiting.add(step.id);
|
|
2271
|
+
path6.push(step.id);
|
|
2272
|
+
const frames = [{ id: step.id, next: adjacency.get(step.id) ?? [], index: 0 }];
|
|
2273
|
+
while (frames.length > 0) {
|
|
2274
|
+
const frame = frames[frames.length - 1];
|
|
2275
|
+
if (frame.index < frame.next.length) {
|
|
2276
|
+
const next = frame.next[frame.index];
|
|
2277
|
+
frame.index += 1;
|
|
2278
|
+
if (visiting.has(next)) return [...path6.slice(path6.indexOf(next)), next];
|
|
2279
|
+
if (visited.has(next)) continue;
|
|
2280
|
+
visiting.add(next);
|
|
2281
|
+
path6.push(next);
|
|
2282
|
+
frames.push({ id: next, next: adjacency.get(next) ?? [], index: 0 });
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
frames.pop();
|
|
2286
|
+
path6.pop();
|
|
2287
|
+
visiting.delete(frame.id);
|
|
2288
|
+
visited.add(frame.id);
|
|
2289
|
+
}
|
|
2269
2290
|
}
|
|
2270
2291
|
return null;
|
|
2271
2292
|
}
|
|
@@ -4393,6 +4414,8 @@ var GOVERNANCE_OBSERVATION_CATEGORIES = [
|
|
|
4393
4414
|
"daemon_attachment_broker_lifecycle",
|
|
4394
4415
|
"daemon_shutdown_requested"
|
|
4395
4416
|
];
|
|
4417
|
+
var GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE = 50;
|
|
4418
|
+
var GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE = 100;
|
|
4396
4419
|
var GovernanceStoreCorruptionError = class extends Error {
|
|
4397
4420
|
constructor(message) {
|
|
4398
4421
|
super(message);
|
|
@@ -4590,21 +4613,47 @@ var SqliteGovernanceEventStore = class _SqliteGovernanceEventStore {
|
|
|
4590
4613
|
throw error;
|
|
4591
4614
|
}
|
|
4592
4615
|
}
|
|
4593
|
-
|
|
4616
|
+
/**
|
|
4617
|
+
* Paginated, category-filtered observation read.
|
|
4618
|
+
*
|
|
4619
|
+
* The observations table is append-only — its UPDATE and DELETE triggers say
|
|
4620
|
+
* so — and therefore grows without bound for the lifetime of a project. The
|
|
4621
|
+
* previous unpaginated read loaded every row a project had ever recorded and
|
|
4622
|
+
* left the caller to filter in JavaScript, which cost 148 MB and 400 ms at
|
|
4623
|
+
* 100 000 rows and produced a 8.9 MB response: already past the 8 MB IPC
|
|
4624
|
+
* frame cap, so the endpoint could not succeed at all past that point. Both
|
|
4625
|
+
* the category filter and the page window belong in SQL, matching what
|
|
4626
|
+
* `readEvidenceCandidateObservations` already does.
|
|
4627
|
+
*/
|
|
4628
|
+
readObservationsPage(options) {
|
|
4594
4629
|
this.assertOpen();
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4630
|
+
if (!Number.isSafeInteger(options.afterSequence) || options.afterSequence < 0) {
|
|
4631
|
+
throw new Error("Observation cursor must be a non-negative safe integer.");
|
|
4632
|
+
}
|
|
4633
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE + 1) {
|
|
4634
|
+
throw new Error(
|
|
4635
|
+
`Observation query limit must be between 1 and ${GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE + 1}.`
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
if (options.categories.length === 0) return Object.freeze([]);
|
|
4639
|
+
const parameters = [options.projectId];
|
|
4640
|
+
let where = "project_id = ?";
|
|
4641
|
+
if (options.taskId !== void 0) {
|
|
4642
|
+
where += " AND task_id = ?";
|
|
4643
|
+
parameters.push(options.taskId);
|
|
4644
|
+
}
|
|
4645
|
+
where += ` AND category IN (${options.categories.map(() => "?").join(",")})`;
|
|
4646
|
+
parameters.push(...options.categories);
|
|
4647
|
+
where += " AND sequence > ?";
|
|
4648
|
+
parameters.push(options.afterSequence, options.limit);
|
|
4649
|
+
const rows = this.db.prepare(
|
|
4602
4650
|
`SELECT sequence, observation_id, project_id, task_id, source, category,
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4651
|
+
observation_json, observation_hash, observed_at, recorded_at
|
|
4652
|
+
FROM governance_observations
|
|
4653
|
+
WHERE ${where}
|
|
4654
|
+
ORDER BY sequence
|
|
4655
|
+
LIMIT ?`
|
|
4656
|
+
).all(...parameters);
|
|
4608
4657
|
return Object.freeze(rows.map((row) => this.observationFromRow(row)));
|
|
4609
4658
|
}
|
|
4610
4659
|
readEvidenceCandidateObservations(projectId, taskId, options) {
|
|
@@ -4878,7 +4927,18 @@ var MAX_NESTING_DEPTH = 32;
|
|
|
4878
4927
|
var MAX_ARRAY_ITEMS = 1e4;
|
|
4879
4928
|
var MAX_OBJECT_KEYS = 1e3;
|
|
4880
4929
|
var MAX_STRING_LENGTH = 1e6;
|
|
4930
|
+
var MAX_ISSUES = 100;
|
|
4881
4931
|
function issue2(issues, code, path6, message) {
|
|
4932
|
+
if (issues.length >= MAX_ISSUES) {
|
|
4933
|
+
if (issues.length === MAX_ISSUES) {
|
|
4934
|
+
issues.push({
|
|
4935
|
+
code: "resource_limit",
|
|
4936
|
+
path: "$",
|
|
4937
|
+
message: `Validation stopped after ${MAX_ISSUES} issues.`
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
return;
|
|
4941
|
+
}
|
|
4882
4942
|
issues.push({ code, path: path6, message });
|
|
4883
4943
|
}
|
|
4884
4944
|
function plainRecord4(value, path6, issues) {
|
|
@@ -4970,23 +5030,24 @@ function enumArray(value, allowed, path6, issues) {
|
|
|
4970
5030
|
(entry, entryPath, target) => enumValue(entry, allowed, entryPath, target)
|
|
4971
5031
|
);
|
|
4972
5032
|
}
|
|
4973
|
-
function
|
|
5033
|
+
function boundedCapabilityArray(value, allowed, noun, path6, issues) {
|
|
4974
5034
|
if (!Array.isArray(value)) {
|
|
4975
5035
|
issue2(issues, "expected_array", path6, `${path6} must be an array.`);
|
|
4976
5036
|
return;
|
|
4977
5037
|
}
|
|
4978
|
-
if (value.length === 0 || value.length >
|
|
5038
|
+
if (value.length === 0 || value.length > allowed.length) {
|
|
4979
5039
|
issue2(
|
|
4980
5040
|
issues,
|
|
4981
5041
|
"resource_limit",
|
|
4982
5042
|
path6,
|
|
4983
|
-
`${path6} must contain between 1 and ${
|
|
5043
|
+
`${path6} must contain between 1 and ${allowed.length} ${noun}.`
|
|
4984
5044
|
);
|
|
5045
|
+
return;
|
|
4985
5046
|
}
|
|
4986
5047
|
const seen = /* @__PURE__ */ new Set();
|
|
4987
5048
|
for (let index = 0; index < value.length; index += 1) {
|
|
4988
5049
|
const entryPath = `${path6}[${index}]`;
|
|
4989
|
-
enumValue(value[index],
|
|
5050
|
+
enumValue(value[index], allowed, entryPath, issues);
|
|
4990
5051
|
if (typeof value[index] === "string") {
|
|
4991
5052
|
if (seen.has(value[index])) {
|
|
4992
5053
|
issue2(issues, "semantic_invalid", entryPath, `${entryPath} duplicates a capability.`);
|
|
@@ -4995,30 +5056,17 @@ function capabilityArray(value, path6, issues) {
|
|
|
4995
5056
|
}
|
|
4996
5057
|
}
|
|
4997
5058
|
}
|
|
5059
|
+
function capabilityArray(value, path6, issues) {
|
|
5060
|
+
boundedCapabilityArray(value, GOVERNANCE_SERVICE_CAPABILITIES, "capabilities", path6, issues);
|
|
5061
|
+
}
|
|
4998
5062
|
function runtimeModelCapabilityArray(value, path6, issues) {
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
"resource_limit",
|
|
5007
|
-
path6,
|
|
5008
|
-
`${path6} must contain between 1 and ${GOVERNANCE_RUNTIME_MODEL_CAPABILITIES.length} model-safe capabilities.`
|
|
5009
|
-
);
|
|
5010
|
-
}
|
|
5011
|
-
const seen = /* @__PURE__ */ new Set();
|
|
5012
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
5013
|
-
const entryPath = `${path6}[${index}]`;
|
|
5014
|
-
enumValue(value[index], GOVERNANCE_RUNTIME_MODEL_CAPABILITIES, entryPath, issues);
|
|
5015
|
-
if (typeof value[index] === "string") {
|
|
5016
|
-
if (seen.has(value[index])) {
|
|
5017
|
-
issue2(issues, "semantic_invalid", entryPath, `${entryPath} duplicates a capability.`);
|
|
5018
|
-
}
|
|
5019
|
-
seen.add(value[index]);
|
|
5020
|
-
}
|
|
5021
|
-
}
|
|
5063
|
+
boundedCapabilityArray(
|
|
5064
|
+
value,
|
|
5065
|
+
GOVERNANCE_RUNTIME_MODEL_CAPABILITIES,
|
|
5066
|
+
"model-safe capabilities",
|
|
5067
|
+
path6,
|
|
5068
|
+
issues
|
|
5069
|
+
);
|
|
5022
5070
|
}
|
|
5023
5071
|
function jsonValue(value, path6, issues, depth = 0) {
|
|
5024
5072
|
if (depth > MAX_NESTING_DEPTH) {
|
|
@@ -5328,6 +5376,21 @@ function validateObservation(value, path6, issues) {
|
|
|
5328
5376
|
const payload = plainRecord4(record.payload, `${path6}.payload`, issues);
|
|
5329
5377
|
if (payload) jsonValue(payload, `${path6}.payload`, issues);
|
|
5330
5378
|
}
|
|
5379
|
+
function observationPageWindow(record, issues) {
|
|
5380
|
+
if (record.afterSequence !== void 0) {
|
|
5381
|
+
integerValue(record.afterSequence, "$.afterSequence", issues);
|
|
5382
|
+
}
|
|
5383
|
+
if (record.limit === void 0) return;
|
|
5384
|
+
integerValue(record.limit, "$.limit", issues, 1);
|
|
5385
|
+
if (typeof record.limit === "number" && record.limit > GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE) {
|
|
5386
|
+
issue2(
|
|
5387
|
+
issues,
|
|
5388
|
+
"resource_limit",
|
|
5389
|
+
"$.limit",
|
|
5390
|
+
`$.limit must not exceed ${GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE}.`
|
|
5391
|
+
);
|
|
5392
|
+
}
|
|
5393
|
+
}
|
|
5331
5394
|
function deepFreeze4(value) {
|
|
5332
5395
|
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
5333
5396
|
for (const nested of Object.values(value)) deepFreeze4(nested);
|
|
@@ -5360,8 +5423,14 @@ function decodeGovernanceServiceRequest(input) {
|
|
|
5360
5423
|
stringValue(record.commandId, "$.commandId", issues);
|
|
5361
5424
|
break;
|
|
5362
5425
|
case "read_observations":
|
|
5363
|
-
exactKeys2(
|
|
5426
|
+
exactKeys2(
|
|
5427
|
+
record,
|
|
5428
|
+
["protocolVersion", "requestId", "type", "taskId", "afterSequence", "limit"],
|
|
5429
|
+
"$",
|
|
5430
|
+
issues
|
|
5431
|
+
);
|
|
5364
5432
|
if (record.taskId !== void 0) stringValue(record.taskId, "$.taskId", issues);
|
|
5433
|
+
observationPageWindow(record, issues);
|
|
5365
5434
|
break;
|
|
5366
5435
|
case "read_evidence_candidates":
|
|
5367
5436
|
exactKeys2(
|
|
@@ -5387,6 +5456,14 @@ function decodeGovernanceServiceRequest(input) {
|
|
|
5387
5456
|
}
|
|
5388
5457
|
break;
|
|
5389
5458
|
case "read_audit_observations":
|
|
5459
|
+
exactKeys2(
|
|
5460
|
+
record,
|
|
5461
|
+
["protocolVersion", "requestId", "type", "afterSequence", "limit"],
|
|
5462
|
+
"$",
|
|
5463
|
+
issues
|
|
5464
|
+
);
|
|
5465
|
+
observationPageWindow(record, issues);
|
|
5466
|
+
break;
|
|
5390
5467
|
case "read_own_capability_grant":
|
|
5391
5468
|
case "read_daemon_status":
|
|
5392
5469
|
exactKeys2(record, ["protocolVersion", "requestId", "type"], "$", issues);
|
|
@@ -6577,6 +6654,12 @@ function fail(requestId, code, message, details) {
|
|
|
6577
6654
|
function reservedAuditCategory(category) {
|
|
6578
6655
|
return category.startsWith("capability_grant_") || category.startsWith("daemon_");
|
|
6579
6656
|
}
|
|
6657
|
+
var AUDIT_OBSERVATION_CATEGORIES = Object.freeze(
|
|
6658
|
+
GOVERNANCE_OBSERVATION_CATEGORIES.filter((category) => reservedAuditCategory(category))
|
|
6659
|
+
);
|
|
6660
|
+
var TASK_OBSERVATION_CATEGORIES = Object.freeze(
|
|
6661
|
+
GOVERNANCE_OBSERVATION_CATEGORIES.filter((category) => !reservedAuditCategory(category))
|
|
6662
|
+
);
|
|
6580
6663
|
var GovernanceProjectService = class {
|
|
6581
6664
|
constructor(projectId, store, resolveDecisionContext = () => ({})) {
|
|
6582
6665
|
this.projectId = projectId;
|
|
@@ -6704,14 +6787,13 @@ var GovernanceProjectService = class {
|
|
|
6704
6787
|
};
|
|
6705
6788
|
}
|
|
6706
6789
|
case "read_observations":
|
|
6707
|
-
return
|
|
6708
|
-
ok: true,
|
|
6790
|
+
return this.observationPage(
|
|
6709
6791
|
requestId,
|
|
6710
|
-
|
|
6711
|
-
|
|
6712
|
-
|
|
6713
|
-
|
|
6714
|
-
|
|
6792
|
+
TASK_OBSERVATION_CATEGORIES,
|
|
6793
|
+
request.taskId,
|
|
6794
|
+
request.afterSequence,
|
|
6795
|
+
request.limit
|
|
6796
|
+
);
|
|
6715
6797
|
case "read_evidence_candidates": {
|
|
6716
6798
|
const limit = request.limit ?? GOVERNANCE_EVIDENCE_CANDIDATE_DEFAULT_PAGE_SIZE;
|
|
6717
6799
|
const rows = this.store.readEvidenceCandidateObservations(this.projectId, request.taskId, {
|
|
@@ -6735,14 +6817,13 @@ var GovernanceProjectService = class {
|
|
|
6735
6817
|
};
|
|
6736
6818
|
}
|
|
6737
6819
|
case "read_audit_observations":
|
|
6738
|
-
return
|
|
6739
|
-
ok: true,
|
|
6820
|
+
return this.observationPage(
|
|
6740
6821
|
requestId,
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6822
|
+
AUDIT_OBSERVATION_CATEGORIES,
|
|
6823
|
+
void 0,
|
|
6824
|
+
request.afterSequence,
|
|
6825
|
+
request.limit
|
|
6826
|
+
);
|
|
6746
6827
|
case "submit_command": {
|
|
6747
6828
|
if (!this.commandBelongsToProject(request.command)) {
|
|
6748
6829
|
return fail(
|
|
@@ -6804,6 +6885,31 @@ var GovernanceProjectService = class {
|
|
|
6804
6885
|
);
|
|
6805
6886
|
}
|
|
6806
6887
|
}
|
|
6888
|
+
/**
|
|
6889
|
+
* Shared body for both observation reads. Fetches one row past the page so
|
|
6890
|
+
* the presence of a further page is known without a second query — the same
|
|
6891
|
+
* convention `read_evidence_candidates` uses.
|
|
6892
|
+
*/
|
|
6893
|
+
observationPage(requestId, categories, taskId, afterSequence, limit) {
|
|
6894
|
+
const pageSize = limit ?? GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE;
|
|
6895
|
+
const rows = this.store.readObservationsPage({
|
|
6896
|
+
projectId: this.projectId,
|
|
6897
|
+
...taskId === void 0 ? {} : { taskId },
|
|
6898
|
+
categories,
|
|
6899
|
+
afterSequence: afterSequence ?? 0,
|
|
6900
|
+
limit: pageSize + 1
|
|
6901
|
+
});
|
|
6902
|
+
const page = rows.slice(0, pageSize);
|
|
6903
|
+
return {
|
|
6904
|
+
ok: true,
|
|
6905
|
+
requestId,
|
|
6906
|
+
result: {
|
|
6907
|
+
type: "observations",
|
|
6908
|
+
observations: page,
|
|
6909
|
+
nextAfterSequence: rows.length > pageSize && page.length > 0 ? page.at(-1)?.sequence ?? null : null
|
|
6910
|
+
}
|
|
6911
|
+
};
|
|
6912
|
+
}
|
|
6807
6913
|
commandBelongsToProject(command) {
|
|
6808
6914
|
if (command.type === "create_task") {
|
|
6809
6915
|
return command.projectId === this.projectId && command.contract.projectId === this.projectId;
|
|
@@ -7078,12 +7184,14 @@ var GovernanceProjectServer = class {
|
|
|
7078
7184
|
else socket.destroy();
|
|
7079
7185
|
}
|
|
7080
7186
|
accept(socket) {
|
|
7081
|
-
|
|
7187
|
+
const chunks = [];
|
|
7188
|
+
let size = 0;
|
|
7082
7189
|
let handled = false;
|
|
7083
7190
|
socket.on("data", (chunk) => {
|
|
7084
7191
|
if (handled) return;
|
|
7085
|
-
|
|
7086
|
-
|
|
7192
|
+
const newline = chunk.indexOf(10);
|
|
7193
|
+
const frameBytes = size + (newline < 0 ? chunk.byteLength : newline);
|
|
7194
|
+
if (frameBytes > GOVERNANCE_IPC_MAX_FRAME_BYTES) {
|
|
7087
7195
|
handled = true;
|
|
7088
7196
|
this.send(
|
|
7089
7197
|
socket,
|
|
@@ -7091,10 +7199,14 @@ var GovernanceProjectServer = class {
|
|
|
7091
7199
|
);
|
|
7092
7200
|
return;
|
|
7093
7201
|
}
|
|
7094
|
-
|
|
7095
|
-
|
|
7202
|
+
if (newline < 0) {
|
|
7203
|
+
chunks.push(chunk);
|
|
7204
|
+
size = frameBytes;
|
|
7205
|
+
return;
|
|
7206
|
+
}
|
|
7096
7207
|
handled = true;
|
|
7097
|
-
|
|
7208
|
+
chunks.push(chunk.subarray(0, newline));
|
|
7209
|
+
const line = Buffer.concat(chunks).toString("utf8").replace(/\r$/, "");
|
|
7098
7210
|
this.handleLine(socket, line);
|
|
7099
7211
|
});
|
|
7100
7212
|
}
|
|
@@ -7325,10 +7437,10 @@ var GovernanceCompatibilityRuntime = class {
|
|
|
7325
7437
|
this.#closePromise = this.revokeModelGrant().then((response) => {
|
|
7326
7438
|
if (response.ok && response.result.type === "capability_grant_revoked") {
|
|
7327
7439
|
this.#closed = true;
|
|
7328
|
-
adminSession.stop();
|
|
7329
7440
|
}
|
|
7330
7441
|
return response;
|
|
7331
7442
|
}).finally(() => {
|
|
7443
|
+
adminSession.stop();
|
|
7332
7444
|
this.#closePromise = void 0;
|
|
7333
7445
|
});
|
|
7334
7446
|
return this.#closePromise;
|
|
@@ -8348,6 +8460,8 @@ export {
|
|
|
8348
8460
|
GOVERNANCE_MANAGEMENT_RECEIPT_MAX_ENTRIES,
|
|
8349
8461
|
GOVERNANCE_MANAGEMENT_RECEIPT_TTL_MS,
|
|
8350
8462
|
GOVERNANCE_OBSERVATION_CATEGORIES,
|
|
8463
|
+
GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE,
|
|
8464
|
+
GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE,
|
|
8351
8465
|
GOVERNANCE_RUNTIME_ATTACHMENT_MAX_TTL_MS,
|
|
8352
8466
|
GOVERNANCE_SERVICE_CAPABILITIES,
|
|
8353
8467
|
GOVERNANCE_SERVICE_PROTOCOL_VERSION,
|
package/dist/project-daemon.js
CHANGED
|
@@ -1333,27 +1333,29 @@ function findCycle(steps, edges) {
|
|
|
1333
1333
|
for (const edge of edges) adjacency.get(edge.fromStepId)?.push(edge.toStepId);
|
|
1334
1334
|
const visiting = /* @__PURE__ */ new Set();
|
|
1335
1335
|
const visited = /* @__PURE__ */ new Set();
|
|
1336
|
-
const
|
|
1337
|
-
const visit = (stepId) => {
|
|
1338
|
-
if (visiting.has(stepId)) {
|
|
1339
|
-
const cycleStart = stack.indexOf(stepId);
|
|
1340
|
-
return [...stack.slice(cycleStart), stepId];
|
|
1341
|
-
}
|
|
1342
|
-
if (visited.has(stepId)) return null;
|
|
1343
|
-
visiting.add(stepId);
|
|
1344
|
-
stack.push(stepId);
|
|
1345
|
-
for (const next of adjacency.get(stepId) ?? []) {
|
|
1346
|
-
const cycle = visit(next);
|
|
1347
|
-
if (cycle) return cycle;
|
|
1348
|
-
}
|
|
1349
|
-
stack.pop();
|
|
1350
|
-
visiting.delete(stepId);
|
|
1351
|
-
visited.add(stepId);
|
|
1352
|
-
return null;
|
|
1353
|
-
};
|
|
1336
|
+
const path5 = [];
|
|
1354
1337
|
for (const step of steps) {
|
|
1355
|
-
|
|
1356
|
-
|
|
1338
|
+
if (visited.has(step.id)) continue;
|
|
1339
|
+
visiting.add(step.id);
|
|
1340
|
+
path5.push(step.id);
|
|
1341
|
+
const frames = [{ id: step.id, next: adjacency.get(step.id) ?? [], index: 0 }];
|
|
1342
|
+
while (frames.length > 0) {
|
|
1343
|
+
const frame = frames[frames.length - 1];
|
|
1344
|
+
if (frame.index < frame.next.length) {
|
|
1345
|
+
const next = frame.next[frame.index];
|
|
1346
|
+
frame.index += 1;
|
|
1347
|
+
if (visiting.has(next)) return [...path5.slice(path5.indexOf(next)), next];
|
|
1348
|
+
if (visited.has(next)) continue;
|
|
1349
|
+
visiting.add(next);
|
|
1350
|
+
path5.push(next);
|
|
1351
|
+
frames.push({ id: next, next: adjacency.get(next) ?? [], index: 0 });
|
|
1352
|
+
continue;
|
|
1353
|
+
}
|
|
1354
|
+
frames.pop();
|
|
1355
|
+
path5.pop();
|
|
1356
|
+
visiting.delete(frame.id);
|
|
1357
|
+
visited.add(frame.id);
|
|
1358
|
+
}
|
|
1357
1359
|
}
|
|
1358
1360
|
return null;
|
|
1359
1361
|
}
|
|
@@ -2907,6 +2909,8 @@ var GOVERNANCE_OBSERVATION_CATEGORIES = [
|
|
|
2907
2909
|
"daemon_attachment_broker_lifecycle",
|
|
2908
2910
|
"daemon_shutdown_requested"
|
|
2909
2911
|
];
|
|
2912
|
+
var GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE = 50;
|
|
2913
|
+
var GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE = 100;
|
|
2910
2914
|
var GovernanceStoreCorruptionError = class extends Error {
|
|
2911
2915
|
constructor(message) {
|
|
2912
2916
|
super(message);
|
|
@@ -3104,21 +3108,47 @@ var SqliteGovernanceEventStore = class _SqliteGovernanceEventStore {
|
|
|
3104
3108
|
throw error;
|
|
3105
3109
|
}
|
|
3106
3110
|
}
|
|
3107
|
-
|
|
3111
|
+
/**
|
|
3112
|
+
* Paginated, category-filtered observation read.
|
|
3113
|
+
*
|
|
3114
|
+
* The observations table is append-only — its UPDATE and DELETE triggers say
|
|
3115
|
+
* so — and therefore grows without bound for the lifetime of a project. The
|
|
3116
|
+
* previous unpaginated read loaded every row a project had ever recorded and
|
|
3117
|
+
* left the caller to filter in JavaScript, which cost 148 MB and 400 ms at
|
|
3118
|
+
* 100 000 rows and produced a 8.9 MB response: already past the 8 MB IPC
|
|
3119
|
+
* frame cap, so the endpoint could not succeed at all past that point. Both
|
|
3120
|
+
* the category filter and the page window belong in SQL, matching what
|
|
3121
|
+
* `readEvidenceCandidateObservations` already does.
|
|
3122
|
+
*/
|
|
3123
|
+
readObservationsPage(options) {
|
|
3108
3124
|
this.assertOpen();
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3125
|
+
if (!Number.isSafeInteger(options.afterSequence) || options.afterSequence < 0) {
|
|
3126
|
+
throw new Error("Observation cursor must be a non-negative safe integer.");
|
|
3127
|
+
}
|
|
3128
|
+
if (!Number.isSafeInteger(options.limit) || options.limit < 1 || options.limit > GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE + 1) {
|
|
3129
|
+
throw new Error(
|
|
3130
|
+
`Observation query limit must be between 1 and ${GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE + 1}.`
|
|
3131
|
+
);
|
|
3132
|
+
}
|
|
3133
|
+
if (options.categories.length === 0) return Object.freeze([]);
|
|
3134
|
+
const parameters = [options.projectId];
|
|
3135
|
+
let where = "project_id = ?";
|
|
3136
|
+
if (options.taskId !== void 0) {
|
|
3137
|
+
where += " AND task_id = ?";
|
|
3138
|
+
parameters.push(options.taskId);
|
|
3139
|
+
}
|
|
3140
|
+
where += ` AND category IN (${options.categories.map(() => "?").join(",")})`;
|
|
3141
|
+
parameters.push(...options.categories);
|
|
3142
|
+
where += " AND sequence > ?";
|
|
3143
|
+
parameters.push(options.afterSequence, options.limit);
|
|
3144
|
+
const rows = this.db.prepare(
|
|
3116
3145
|
`SELECT sequence, observation_id, project_id, task_id, source, category,
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3146
|
+
observation_json, observation_hash, observed_at, recorded_at
|
|
3147
|
+
FROM governance_observations
|
|
3148
|
+
WHERE ${where}
|
|
3149
|
+
ORDER BY sequence
|
|
3150
|
+
LIMIT ?`
|
|
3151
|
+
).all(...parameters);
|
|
3122
3152
|
return Object.freeze(rows.map((row) => this.observationFromRow(row)));
|
|
3123
3153
|
}
|
|
3124
3154
|
readEvidenceCandidateObservations(projectId, taskId, options) {
|
|
@@ -3379,7 +3409,18 @@ var MAX_NESTING_DEPTH = 32;
|
|
|
3379
3409
|
var MAX_ARRAY_ITEMS = 1e4;
|
|
3380
3410
|
var MAX_OBJECT_KEYS = 1e3;
|
|
3381
3411
|
var MAX_STRING_LENGTH = 1e6;
|
|
3412
|
+
var MAX_ISSUES = 100;
|
|
3382
3413
|
function issue(issues, code, path5, message) {
|
|
3414
|
+
if (issues.length >= MAX_ISSUES) {
|
|
3415
|
+
if (issues.length === MAX_ISSUES) {
|
|
3416
|
+
issues.push({
|
|
3417
|
+
code: "resource_limit",
|
|
3418
|
+
path: "$",
|
|
3419
|
+
message: `Validation stopped after ${MAX_ISSUES} issues.`
|
|
3420
|
+
});
|
|
3421
|
+
}
|
|
3422
|
+
return;
|
|
3423
|
+
}
|
|
3383
3424
|
issues.push({ code, path: path5, message });
|
|
3384
3425
|
}
|
|
3385
3426
|
function plainRecord4(value, path5, issues) {
|
|
@@ -3471,23 +3512,24 @@ function enumArray(value, allowed, path5, issues) {
|
|
|
3471
3512
|
(entry, entryPath, target) => enumValue(entry, allowed, entryPath, target)
|
|
3472
3513
|
);
|
|
3473
3514
|
}
|
|
3474
|
-
function
|
|
3515
|
+
function boundedCapabilityArray(value, allowed, noun, path5, issues) {
|
|
3475
3516
|
if (!Array.isArray(value)) {
|
|
3476
3517
|
issue(issues, "expected_array", path5, `${path5} must be an array.`);
|
|
3477
3518
|
return;
|
|
3478
3519
|
}
|
|
3479
|
-
if (value.length === 0 || value.length >
|
|
3520
|
+
if (value.length === 0 || value.length > allowed.length) {
|
|
3480
3521
|
issue(
|
|
3481
3522
|
issues,
|
|
3482
3523
|
"resource_limit",
|
|
3483
3524
|
path5,
|
|
3484
|
-
`${path5} must contain between 1 and ${
|
|
3525
|
+
`${path5} must contain between 1 and ${allowed.length} ${noun}.`
|
|
3485
3526
|
);
|
|
3527
|
+
return;
|
|
3486
3528
|
}
|
|
3487
3529
|
const seen = /* @__PURE__ */ new Set();
|
|
3488
3530
|
for (let index = 0; index < value.length; index += 1) {
|
|
3489
3531
|
const entryPath = `${path5}[${index}]`;
|
|
3490
|
-
enumValue(value[index],
|
|
3532
|
+
enumValue(value[index], allowed, entryPath, issues);
|
|
3491
3533
|
if (typeof value[index] === "string") {
|
|
3492
3534
|
if (seen.has(value[index])) {
|
|
3493
3535
|
issue(issues, "semantic_invalid", entryPath, `${entryPath} duplicates a capability.`);
|
|
@@ -3496,30 +3538,17 @@ function capabilityArray(value, path5, issues) {
|
|
|
3496
3538
|
}
|
|
3497
3539
|
}
|
|
3498
3540
|
}
|
|
3541
|
+
function capabilityArray(value, path5, issues) {
|
|
3542
|
+
boundedCapabilityArray(value, GOVERNANCE_SERVICE_CAPABILITIES, "capabilities", path5, issues);
|
|
3543
|
+
}
|
|
3499
3544
|
function runtimeModelCapabilityArray(value, path5, issues) {
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
"resource_limit",
|
|
3508
|
-
path5,
|
|
3509
|
-
`${path5} must contain between 1 and ${GOVERNANCE_RUNTIME_MODEL_CAPABILITIES.length} model-safe capabilities.`
|
|
3510
|
-
);
|
|
3511
|
-
}
|
|
3512
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3513
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
3514
|
-
const entryPath = `${path5}[${index}]`;
|
|
3515
|
-
enumValue(value[index], GOVERNANCE_RUNTIME_MODEL_CAPABILITIES, entryPath, issues);
|
|
3516
|
-
if (typeof value[index] === "string") {
|
|
3517
|
-
if (seen.has(value[index])) {
|
|
3518
|
-
issue(issues, "semantic_invalid", entryPath, `${entryPath} duplicates a capability.`);
|
|
3519
|
-
}
|
|
3520
|
-
seen.add(value[index]);
|
|
3521
|
-
}
|
|
3522
|
-
}
|
|
3545
|
+
boundedCapabilityArray(
|
|
3546
|
+
value,
|
|
3547
|
+
GOVERNANCE_RUNTIME_MODEL_CAPABILITIES,
|
|
3548
|
+
"model-safe capabilities",
|
|
3549
|
+
path5,
|
|
3550
|
+
issues
|
|
3551
|
+
);
|
|
3523
3552
|
}
|
|
3524
3553
|
function jsonValue(value, path5, issues, depth = 0) {
|
|
3525
3554
|
if (depth > MAX_NESTING_DEPTH) {
|
|
@@ -3829,6 +3858,21 @@ function validateObservation(value, path5, issues) {
|
|
|
3829
3858
|
const payload = plainRecord4(record.payload, `${path5}.payload`, issues);
|
|
3830
3859
|
if (payload) jsonValue(payload, `${path5}.payload`, issues);
|
|
3831
3860
|
}
|
|
3861
|
+
function observationPageWindow(record, issues) {
|
|
3862
|
+
if (record.afterSequence !== void 0) {
|
|
3863
|
+
integerValue(record.afterSequence, "$.afterSequence", issues);
|
|
3864
|
+
}
|
|
3865
|
+
if (record.limit === void 0) return;
|
|
3866
|
+
integerValue(record.limit, "$.limit", issues, 1);
|
|
3867
|
+
if (typeof record.limit === "number" && record.limit > GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE) {
|
|
3868
|
+
issue(
|
|
3869
|
+
issues,
|
|
3870
|
+
"resource_limit",
|
|
3871
|
+
"$.limit",
|
|
3872
|
+
`$.limit must not exceed ${GOVERNANCE_OBSERVATION_MAX_PAGE_SIZE}.`
|
|
3873
|
+
);
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3832
3876
|
function deepFreeze4(value) {
|
|
3833
3877
|
if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
|
|
3834
3878
|
for (const nested of Object.values(value)) deepFreeze4(nested);
|
|
@@ -3861,8 +3905,14 @@ function decodeGovernanceServiceRequest(input) {
|
|
|
3861
3905
|
stringValue(record.commandId, "$.commandId", issues);
|
|
3862
3906
|
break;
|
|
3863
3907
|
case "read_observations":
|
|
3864
|
-
exactKeys2(
|
|
3908
|
+
exactKeys2(
|
|
3909
|
+
record,
|
|
3910
|
+
["protocolVersion", "requestId", "type", "taskId", "afterSequence", "limit"],
|
|
3911
|
+
"$",
|
|
3912
|
+
issues
|
|
3913
|
+
);
|
|
3865
3914
|
if (record.taskId !== void 0) stringValue(record.taskId, "$.taskId", issues);
|
|
3915
|
+
observationPageWindow(record, issues);
|
|
3866
3916
|
break;
|
|
3867
3917
|
case "read_evidence_candidates":
|
|
3868
3918
|
exactKeys2(
|
|
@@ -3888,6 +3938,14 @@ function decodeGovernanceServiceRequest(input) {
|
|
|
3888
3938
|
}
|
|
3889
3939
|
break;
|
|
3890
3940
|
case "read_audit_observations":
|
|
3941
|
+
exactKeys2(
|
|
3942
|
+
record,
|
|
3943
|
+
["protocolVersion", "requestId", "type", "afterSequence", "limit"],
|
|
3944
|
+
"$",
|
|
3945
|
+
issues
|
|
3946
|
+
);
|
|
3947
|
+
observationPageWindow(record, issues);
|
|
3948
|
+
break;
|
|
3891
3949
|
case "read_own_capability_grant":
|
|
3892
3950
|
case "read_daemon_status":
|
|
3893
3951
|
exactKeys2(record, ["protocolVersion", "requestId", "type"], "$", issues);
|
|
@@ -4722,12 +4780,22 @@ var GovernanceCapabilityGrantRegistry = class {
|
|
|
4722
4780
|
const remaining = record.expiresAtMs - this.now();
|
|
4723
4781
|
return remaining > 0 ? remaining : null;
|
|
4724
4782
|
}
|
|
4783
|
+
/**
|
|
4784
|
+
* Ordering is deliberately byte-wise rather than locale-aware. Both keys are
|
|
4785
|
+
* ASCII by construction — `issuedAt` is an ISO-8601 timestamp and `grantId`
|
|
4786
|
+
* matches `^[A-Za-z0-9_-]{1,128}$` — so the two orders are identical, but
|
|
4787
|
+
* `localeCompare` would make the order depend on the host's default locale.
|
|
4788
|
+
* This list backs the `list_capability_grants` cursor, and a cursor is only
|
|
4789
|
+
* sound while the order it walks is stable across the requests that walk it.
|
|
4790
|
+
*/
|
|
4725
4791
|
listGrants() {
|
|
4726
4792
|
const now = this.now();
|
|
4727
4793
|
return Object.freeze(
|
|
4728
|
-
[...this.records.values()].map((record) => this.describe(record, now)).sort(
|
|
4729
|
-
(left
|
|
4730
|
-
|
|
4794
|
+
[...this.records.values()].map((record) => this.describe(record, now)).sort((left, right) => {
|
|
4795
|
+
if (left.issuedAt !== right.issuedAt) return left.issuedAt < right.issuedAt ? -1 : 1;
|
|
4796
|
+
if (left.grantId === right.grantId) return 0;
|
|
4797
|
+
return left.grantId < right.grantId ? -1 : 1;
|
|
4798
|
+
})
|
|
4731
4799
|
);
|
|
4732
4800
|
}
|
|
4733
4801
|
parseToken(token) {
|
|
@@ -4871,6 +4939,12 @@ function fail(requestId, code, message, details) {
|
|
|
4871
4939
|
function reservedAuditCategory(category) {
|
|
4872
4940
|
return category.startsWith("capability_grant_") || category.startsWith("daemon_");
|
|
4873
4941
|
}
|
|
4942
|
+
var AUDIT_OBSERVATION_CATEGORIES = Object.freeze(
|
|
4943
|
+
GOVERNANCE_OBSERVATION_CATEGORIES.filter((category) => reservedAuditCategory(category))
|
|
4944
|
+
);
|
|
4945
|
+
var TASK_OBSERVATION_CATEGORIES = Object.freeze(
|
|
4946
|
+
GOVERNANCE_OBSERVATION_CATEGORIES.filter((category) => !reservedAuditCategory(category))
|
|
4947
|
+
);
|
|
4874
4948
|
var GovernanceProjectService = class {
|
|
4875
4949
|
constructor(projectId, store, resolveDecisionContext = () => ({})) {
|
|
4876
4950
|
this.projectId = projectId;
|
|
@@ -4998,14 +5072,13 @@ var GovernanceProjectService = class {
|
|
|
4998
5072
|
};
|
|
4999
5073
|
}
|
|
5000
5074
|
case "read_observations":
|
|
5001
|
-
return
|
|
5002
|
-
ok: true,
|
|
5075
|
+
return this.observationPage(
|
|
5003
5076
|
requestId,
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5077
|
+
TASK_OBSERVATION_CATEGORIES,
|
|
5078
|
+
request.taskId,
|
|
5079
|
+
request.afterSequence,
|
|
5080
|
+
request.limit
|
|
5081
|
+
);
|
|
5009
5082
|
case "read_evidence_candidates": {
|
|
5010
5083
|
const limit = request.limit ?? GOVERNANCE_EVIDENCE_CANDIDATE_DEFAULT_PAGE_SIZE;
|
|
5011
5084
|
const rows = this.store.readEvidenceCandidateObservations(this.projectId, request.taskId, {
|
|
@@ -5029,14 +5102,13 @@ var GovernanceProjectService = class {
|
|
|
5029
5102
|
};
|
|
5030
5103
|
}
|
|
5031
5104
|
case "read_audit_observations":
|
|
5032
|
-
return
|
|
5033
|
-
ok: true,
|
|
5105
|
+
return this.observationPage(
|
|
5034
5106
|
requestId,
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
|
|
5038
|
-
|
|
5039
|
-
|
|
5107
|
+
AUDIT_OBSERVATION_CATEGORIES,
|
|
5108
|
+
void 0,
|
|
5109
|
+
request.afterSequence,
|
|
5110
|
+
request.limit
|
|
5111
|
+
);
|
|
5040
5112
|
case "submit_command": {
|
|
5041
5113
|
if (!this.commandBelongsToProject(request.command)) {
|
|
5042
5114
|
return fail(
|
|
@@ -5098,6 +5170,31 @@ var GovernanceProjectService = class {
|
|
|
5098
5170
|
);
|
|
5099
5171
|
}
|
|
5100
5172
|
}
|
|
5173
|
+
/**
|
|
5174
|
+
* Shared body for both observation reads. Fetches one row past the page so
|
|
5175
|
+
* the presence of a further page is known without a second query — the same
|
|
5176
|
+
* convention `read_evidence_candidates` uses.
|
|
5177
|
+
*/
|
|
5178
|
+
observationPage(requestId, categories, taskId, afterSequence, limit) {
|
|
5179
|
+
const pageSize = limit ?? GOVERNANCE_OBSERVATION_DEFAULT_PAGE_SIZE;
|
|
5180
|
+
const rows = this.store.readObservationsPage({
|
|
5181
|
+
projectId: this.projectId,
|
|
5182
|
+
...taskId === void 0 ? {} : { taskId },
|
|
5183
|
+
categories,
|
|
5184
|
+
afterSequence: afterSequence ?? 0,
|
|
5185
|
+
limit: pageSize + 1
|
|
5186
|
+
});
|
|
5187
|
+
const page = rows.slice(0, pageSize);
|
|
5188
|
+
return {
|
|
5189
|
+
ok: true,
|
|
5190
|
+
requestId,
|
|
5191
|
+
result: {
|
|
5192
|
+
type: "observations",
|
|
5193
|
+
observations: page,
|
|
5194
|
+
nextAfterSequence: rows.length > pageSize && page.length > 0 ? page.at(-1)?.sequence ?? null : null
|
|
5195
|
+
}
|
|
5196
|
+
};
|
|
5197
|
+
}
|
|
5101
5198
|
commandBelongsToProject(command) {
|
|
5102
5199
|
if (command.type === "create_task") {
|
|
5103
5200
|
return command.projectId === this.projectId && command.contract.projectId === this.projectId;
|
|
@@ -5308,12 +5405,14 @@ var GovernanceProjectServer = class {
|
|
|
5308
5405
|
else socket.destroy();
|
|
5309
5406
|
}
|
|
5310
5407
|
accept(socket) {
|
|
5311
|
-
|
|
5408
|
+
const chunks = [];
|
|
5409
|
+
let size = 0;
|
|
5312
5410
|
let handled = false;
|
|
5313
5411
|
socket.on("data", (chunk) => {
|
|
5314
5412
|
if (handled) return;
|
|
5315
|
-
|
|
5316
|
-
|
|
5413
|
+
const newline = chunk.indexOf(10);
|
|
5414
|
+
const frameBytes = size + (newline < 0 ? chunk.byteLength : newline);
|
|
5415
|
+
if (frameBytes > GOVERNANCE_IPC_MAX_FRAME_BYTES) {
|
|
5317
5416
|
handled = true;
|
|
5318
5417
|
this.send(
|
|
5319
5418
|
socket,
|
|
@@ -5321,10 +5420,14 @@ var GovernanceProjectServer = class {
|
|
|
5321
5420
|
);
|
|
5322
5421
|
return;
|
|
5323
5422
|
}
|
|
5324
|
-
|
|
5325
|
-
|
|
5423
|
+
if (newline < 0) {
|
|
5424
|
+
chunks.push(chunk);
|
|
5425
|
+
size = frameBytes;
|
|
5426
|
+
return;
|
|
5427
|
+
}
|
|
5326
5428
|
handled = true;
|
|
5327
|
-
|
|
5429
|
+
chunks.push(chunk.subarray(0, newline));
|
|
5430
|
+
const line = Buffer.concat(chunks).toString("utf8").replace(/\r$/, "");
|
|
5328
5431
|
this.handleLine(socket, line);
|
|
5329
5432
|
});
|
|
5330
5433
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { GovernanceAttachmentBrokerControllerSnapshot } from './attachment-broker-controller.js';
|
|
2
2
|
import type { GovernanceCapabilityGrant, GovernanceServiceCredential } from './capability-grant.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type AppendGovernanceObservationResult, type GovernanceCommandExecution, type GovernanceCommandReceipt, type GovernanceEventStore, type StoredGovernanceObservation } from './event-store.js';
|
|
4
4
|
import { type GovernanceEvidenceCandidateLedgerEntry } from './evidence-candidate.js';
|
|
5
5
|
import { GOVERNANCE_SERVICE_PROTOCOL_VERSION, type GovernanceServiceClientContext, type GovernanceServiceRequest } from './service-protocol.js';
|
|
6
6
|
import type { GovernanceCommand, GovernanceDecisionContext, GovernanceEvent, TaskAggregate } from './task-aggregate.js';
|
|
@@ -24,6 +24,7 @@ export type GovernanceServiceResult = {
|
|
|
24
24
|
} | {
|
|
25
25
|
readonly type: 'observations';
|
|
26
26
|
readonly observations: readonly StoredGovernanceObservation[];
|
|
27
|
+
readonly nextAfterSequence: number | null;
|
|
27
28
|
} | {
|
|
28
29
|
readonly type: 'evidence_candidates';
|
|
29
30
|
readonly taskId: string;
|
|
@@ -116,6 +117,12 @@ export declare class GovernanceProjectService {
|
|
|
116
117
|
handle(request: GovernanceServiceRequest, client: GovernanceServiceClientContext): GovernanceServiceResponse;
|
|
117
118
|
close(): void;
|
|
118
119
|
private handleAuthorized;
|
|
120
|
+
/**
|
|
121
|
+
* Shared body for both observation reads. Fetches one row past the page so
|
|
122
|
+
* the presence of a further page is known without a second query — the same
|
|
123
|
+
* convention `read_evidence_candidates` uses.
|
|
124
|
+
*/
|
|
125
|
+
private observationPage;
|
|
119
126
|
private commandBelongsToProject;
|
|
120
127
|
}
|
|
121
128
|
//# sourceMappingURL=project-service.d.ts.map
|
|
@@ -28,6 +28,8 @@ export type GovernanceServiceRequest = GovernanceServiceRequestMetadata & ({
|
|
|
28
28
|
} | {
|
|
29
29
|
readonly type: 'read_observations';
|
|
30
30
|
readonly taskId?: string | undefined;
|
|
31
|
+
readonly afterSequence?: number | undefined;
|
|
32
|
+
readonly limit?: number | undefined;
|
|
31
33
|
} | {
|
|
32
34
|
readonly type: 'read_evidence_candidates';
|
|
33
35
|
readonly taskId: string;
|
|
@@ -35,6 +37,8 @@ export type GovernanceServiceRequest = GovernanceServiceRequestMetadata & ({
|
|
|
35
37
|
readonly limit?: number | undefined;
|
|
36
38
|
} | {
|
|
37
39
|
readonly type: 'read_audit_observations';
|
|
40
|
+
readonly afterSequence?: number | undefined;
|
|
41
|
+
readonly limit?: number | undefined;
|
|
38
42
|
} | {
|
|
39
43
|
readonly type: 'read_own_capability_grant';
|
|
40
44
|
} | {
|