@aiwg/cli 2026.8.27 → 2026.8.28
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/bin/aiwg.mjs +6 -0
- package/dist/src/a2a/client.js +4 -1
- package/dist/src/a2a/codecs.js +5 -2
- package/dist/src/a2a/protocol.js +12 -1
- package/dist/src/activity-log/cli.js +4 -1
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/fortemi-core-sync.js +37 -0
- package/dist/src/audit/operator-decision.js +9 -25
- package/dist/src/features/catalog.js +3 -2
- package/dist/src/governance/boundary.js +354 -0
- package/dist/src/governance/classification.js +191 -0
- package/dist/src/governance/index.js +5 -0
- package/dist/src/governance/redaction.js +324 -0
- package/dist/src/governance/retention.js +274 -0
- package/dist/src/jobs/executor.js +2 -3
- package/dist/src/ops/cli.js +95 -0
- package/dist/src/serve/dispatch-router.js +1 -1
- package/dist/src/sessions/repository.js +8 -6
- package/package.json +1 -1
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
export const DEFAULT_RETENTION_RULES = [
|
|
3
|
+
{ id: 'raw-audit-short-lived', version: '1', category: 'raw-audit', tier: 'raw', duration: 'P7D', action: 'delete' },
|
|
4
|
+
{ id: 'identity-audit-durable', version: '1', category: 'identity-audit', tier: 'durable', duration: 'P30D', action: 'summarize' },
|
|
5
|
+
{ id: 'network-inventory-durable', version: '1', category: 'network-inventory', tier: 'durable', duration: 'P30D', action: 'archive', archiveSink: 'encrypted-artifact-store' },
|
|
6
|
+
{ id: 'dr-evidence-durable', version: '1', category: 'dr-evidence', tier: 'durable', duration: 'P90D', action: 'archive', archiveSink: 'encrypted-artifact-store' },
|
|
7
|
+
{ id: 'sanitized-summary-durable', version: '1', category: 'sanitized-summary', tier: 'durable', duration: 'P365D', action: 'summarize' },
|
|
8
|
+
{ id: 'generic-durable', version: '1', category: 'generic', tier: 'durable', duration: 'P90D', action: 'summarize' },
|
|
9
|
+
];
|
|
10
|
+
function digest(value) {
|
|
11
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
12
|
+
}
|
|
13
|
+
function parseDuration(value) {
|
|
14
|
+
if (value === null || value === undefined)
|
|
15
|
+
return null;
|
|
16
|
+
const match = /^P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?)?$/.exec(value);
|
|
17
|
+
if (!match || !match.slice(1).some(Boolean))
|
|
18
|
+
throw new Error(`unsupported retention duration '${value}'`);
|
|
19
|
+
const days = Number(match[1] ?? 0);
|
|
20
|
+
const hours = Number(match[2] ?? 0);
|
|
21
|
+
const minutes = Number(match[3] ?? 0);
|
|
22
|
+
const seconds = Number(match[4] ?? 0);
|
|
23
|
+
const milliseconds = (((days * 24 + hours) * 60 + minutes) * 60 + seconds) * 1_000;
|
|
24
|
+
if (!Number.isSafeInteger(milliseconds) || milliseconds < 1)
|
|
25
|
+
throw new Error(`invalid retention duration '${value}'`);
|
|
26
|
+
return milliseconds;
|
|
27
|
+
}
|
|
28
|
+
export function validateRetentionRules(rules) {
|
|
29
|
+
const ids = new Set();
|
|
30
|
+
const actions = new Set(['retain', 'summarize', 'redact-fields', 'archive', 'delete']);
|
|
31
|
+
for (const rule of rules) {
|
|
32
|
+
if (!/^[a-z0-9][a-z0-9.-]{0,127}$/.test(rule.id))
|
|
33
|
+
throw new Error('retention rule IDs must be lowercase stable identifiers');
|
|
34
|
+
if (ids.has(rule.id))
|
|
35
|
+
throw new Error(`duplicate retention rule '${rule.id}'`);
|
|
36
|
+
ids.add(rule.id);
|
|
37
|
+
if (!rule.version.trim())
|
|
38
|
+
throw new Error(`retention rule '${rule.id}' requires a version`);
|
|
39
|
+
if (!actions.has(rule.action))
|
|
40
|
+
throw new Error(`retention rule '${rule.id}' has an unsupported action`);
|
|
41
|
+
if (rule.tier !== undefined && rule.tier !== 'raw' && rule.tier !== 'durable') {
|
|
42
|
+
throw new Error(`retention rule '${rule.id}' has an unsupported tier`);
|
|
43
|
+
}
|
|
44
|
+
if (rule.priority !== undefined && !Number.isSafeInteger(rule.priority)) {
|
|
45
|
+
throw new Error(`retention rule '${rule.id}' priority must be a safe integer`);
|
|
46
|
+
}
|
|
47
|
+
for (const [name, value] of Object.entries({ category: rule.category, classification: rule.classification, sink: rule.sink })) {
|
|
48
|
+
if (value !== undefined && (typeof value !== 'string' || value.length === 0)) {
|
|
49
|
+
throw new Error(`retention rule '${rule.id}' ${name} must be a non-empty string`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
parseDuration(rule.duration);
|
|
53
|
+
if (rule.action === 'redact-fields' && (!rule.redactFields || rule.redactFields.length === 0)) {
|
|
54
|
+
throw new Error(`retention rule '${rule.id}' requires redactFields`);
|
|
55
|
+
}
|
|
56
|
+
if (rule.action === 'archive' && !rule.archiveSink) {
|
|
57
|
+
throw new Error(`retention rule '${rule.id}' requires archiveSink`);
|
|
58
|
+
}
|
|
59
|
+
if (rule.redactFields !== undefined && (!Array.isArray(rule.redactFields) || rule.redactFields.some((field) => typeof field !== 'string' || !field))) {
|
|
60
|
+
throw new Error(`retention rule '${rule.id}' redactFields must contain non-empty strings`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function ruleMatches(rule, input) {
|
|
65
|
+
return (rule.category === undefined || rule.category === input.category)
|
|
66
|
+
&& (rule.classification === undefined || rule.classification === input.classification)
|
|
67
|
+
&& (rule.sink === undefined || rule.sink === input.sinkId)
|
|
68
|
+
&& (rule.tier === undefined || rule.tier === input.tier);
|
|
69
|
+
}
|
|
70
|
+
function specificity(rule) {
|
|
71
|
+
return [rule.category, rule.classification, rule.sink, rule.tier].filter((value) => value !== undefined).length;
|
|
72
|
+
}
|
|
73
|
+
export function resolveRetentionRule(input, rules = [], requestedPolicyId) {
|
|
74
|
+
const candidates = [
|
|
75
|
+
...rules.map((rule) => ({ rule, configured: true })),
|
|
76
|
+
...DEFAULT_RETENTION_RULES.map((rule) => ({ rule, configured: false })),
|
|
77
|
+
]
|
|
78
|
+
.filter(({ rule }) => (requestedPolicyId === undefined || rule.id === requestedPolicyId) && ruleMatches(rule, input))
|
|
79
|
+
.sort((left, right) => (right.rule.priority ?? 0) - (left.rule.priority ?? 0)
|
|
80
|
+
|| specificity(right.rule) - specificity(left.rule)
|
|
81
|
+
|| Number(right.configured) - Number(left.configured)
|
|
82
|
+
|| left.rule.id.localeCompare(right.rule.id));
|
|
83
|
+
const selected = candidates[0]?.rule;
|
|
84
|
+
if (!selected)
|
|
85
|
+
throw new Error(`no retention rule matches ${input.category}/${input.classification}/${input.sinkId}/${input.tier}`);
|
|
86
|
+
parseDuration(selected.duration);
|
|
87
|
+
if (selected.action === 'redact-fields' && (!selected.redactFields || selected.redactFields.length === 0)) {
|
|
88
|
+
throw new Error(`retention rule '${selected.id}' requires redactFields`);
|
|
89
|
+
}
|
|
90
|
+
if (selected.action === 'archive' && !selected.archiveSink) {
|
|
91
|
+
// Built-in archive policies deliberately require the project to select a destination.
|
|
92
|
+
if (!DEFAULT_RETENTION_RULES.some((rule) => rule.id === selected.id)) {
|
|
93
|
+
throw new Error(`retention rule '${selected.id}' requires archiveSink`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return { ...selected };
|
|
97
|
+
}
|
|
98
|
+
export function createEvidenceLifecycle(input) {
|
|
99
|
+
const createdAt = input.createdAt ?? new Date().toISOString();
|
|
100
|
+
const created = Date.parse(createdAt);
|
|
101
|
+
if (!Number.isFinite(created))
|
|
102
|
+
throw new Error('evidence creation time must be valid ISO-8601');
|
|
103
|
+
if (input.tier === 'raw' && !input.rawCaptureReason?.trim()) {
|
|
104
|
+
throw new Error('full raw evidence capture requires an explicit reason');
|
|
105
|
+
}
|
|
106
|
+
if (!input.sink.mutable && input.tier === 'raw') {
|
|
107
|
+
throw new Error(`raw evidence cannot be published to immutable sink '${input.sink.id}'; publish a sanitized summary instead`);
|
|
108
|
+
}
|
|
109
|
+
const immutableSummaryRule = !input.sink.mutable && input.category === 'sanitized-summary'
|
|
110
|
+
? {
|
|
111
|
+
id: 'immutable-sanitized-summary', version: '1', category: 'sanitized-summary',
|
|
112
|
+
sink: input.sink.id, tier: 'durable', duration: null, action: 'retain', priority: 10_000,
|
|
113
|
+
}
|
|
114
|
+
: undefined;
|
|
115
|
+
const rule = resolveRetentionRule({
|
|
116
|
+
category: input.category,
|
|
117
|
+
classification: input.classification,
|
|
118
|
+
sinkId: input.sink.id,
|
|
119
|
+
tier: input.tier,
|
|
120
|
+
}, [...(immutableSummaryRule ? [immutableSummaryRule] : []), ...(input.rules ?? [])], input.requestedPolicyId);
|
|
121
|
+
const duration = parseDuration(rule.duration);
|
|
122
|
+
if (!input.sink.mutable && duration !== null && rule.action !== 'retain') {
|
|
123
|
+
throw new Error(`sink '${input.sink.id}' cannot satisfy finite lifecycle action '${rule.action}'`);
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
schemaVersion: 'ops-evidence-lifecycle.aiwg.io/v1',
|
|
127
|
+
artifactId: input.artifactId.startsWith('sha256:') && /^sha256:[a-f0-9]{64}$/.test(input.artifactId)
|
|
128
|
+
? input.artifactId
|
|
129
|
+
: digest(input.artifactId),
|
|
130
|
+
category: input.category,
|
|
131
|
+
classification: input.classification,
|
|
132
|
+
sinkId: input.sink.id,
|
|
133
|
+
tier: input.tier,
|
|
134
|
+
createdAt,
|
|
135
|
+
policyId: rule.id,
|
|
136
|
+
policyVersion: rule.version,
|
|
137
|
+
dispositionDeadline: duration === null ? null : new Date(created + duration).toISOString(),
|
|
138
|
+
action: rule.action,
|
|
139
|
+
...(rule.redactFields ? { dispositionFields: [...rule.redactFields] } : {}),
|
|
140
|
+
...(rule.archiveSink ? { archiveSink: rule.archiveSink } : {}),
|
|
141
|
+
...(input.rawCaptureReason ? { rawCaptureReasonDigest: digest(input.rawCaptureReason) } : {}),
|
|
142
|
+
holds: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
export function reapplyRetentionPolicy(input) {
|
|
146
|
+
const metadata = createEvidenceLifecycle({
|
|
147
|
+
artifactId: input.record.metadata.artifactId,
|
|
148
|
+
category: input.record.metadata.category,
|
|
149
|
+
classification: input.record.metadata.classification,
|
|
150
|
+
sink: input.sink,
|
|
151
|
+
tier: input.record.metadata.tier,
|
|
152
|
+
rules: input.rules,
|
|
153
|
+
requestedPolicyId: input.requestedPolicyId,
|
|
154
|
+
rawCaptureReason: input.record.metadata.rawCaptureReasonDigest ? 'previously-approved-raw-capture' : undefined,
|
|
155
|
+
createdAt: input.record.metadata.createdAt,
|
|
156
|
+
});
|
|
157
|
+
return {
|
|
158
|
+
payload: input.record.payload,
|
|
159
|
+
metadata: {
|
|
160
|
+
...metadata,
|
|
161
|
+
...(input.record.metadata.rawCaptureReasonDigest
|
|
162
|
+
? { rawCaptureReasonDigest: input.record.metadata.rawCaptureReasonDigest }
|
|
163
|
+
: {}),
|
|
164
|
+
holds: [...input.record.metadata.holds],
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
export function placeEvidenceHold(input) {
|
|
169
|
+
if (!input.holdId || !input.actor || !input.reason.trim())
|
|
170
|
+
throw new Error('hold ID, actor, and reason are required');
|
|
171
|
+
if (input.record.metadata.holds.some((hold) => hold.id === input.holdId && !hold.releasedAt)) {
|
|
172
|
+
throw new Error(`hold '${input.holdId}' is already active`);
|
|
173
|
+
}
|
|
174
|
+
const occurredAt = (input.now ?? new Date()).toISOString();
|
|
175
|
+
const reasonDigest = digest(input.reason);
|
|
176
|
+
const hold = { id: input.holdId, actor: input.actor, reasonDigest, placedAt: occurredAt };
|
|
177
|
+
return {
|
|
178
|
+
record: {
|
|
179
|
+
payload: input.record.payload,
|
|
180
|
+
metadata: { ...input.record.metadata, holds: [...input.record.metadata.holds, hold] },
|
|
181
|
+
},
|
|
182
|
+
audit: {
|
|
183
|
+
schemaVersion: 'ops-evidence-hold.aiwg.io/v1', eventId: randomUUID(),
|
|
184
|
+
artifactId: input.record.metadata.artifactId, holdId: input.holdId,
|
|
185
|
+
action: 'placed', actor: input.actor, reasonDigest, occurredAt,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
export function releaseEvidenceHold(input) {
|
|
190
|
+
if (!input.actor || !input.reason.trim())
|
|
191
|
+
throw new Error('release actor and reason are required');
|
|
192
|
+
const occurredAt = (input.now ?? new Date()).toISOString();
|
|
193
|
+
const reasonDigest = digest(input.reason);
|
|
194
|
+
let released = false;
|
|
195
|
+
const holds = input.record.metadata.holds.map((hold) => {
|
|
196
|
+
if (hold.id !== input.holdId || hold.releasedAt)
|
|
197
|
+
return hold;
|
|
198
|
+
released = true;
|
|
199
|
+
return { ...hold, releasedAt: occurredAt, releasedBy: input.actor, releaseReasonDigest: reasonDigest };
|
|
200
|
+
});
|
|
201
|
+
if (!released)
|
|
202
|
+
throw new Error(`active hold '${input.holdId}' was not found`);
|
|
203
|
+
return {
|
|
204
|
+
record: { payload: input.record.payload, metadata: { ...input.record.metadata, holds } },
|
|
205
|
+
audit: {
|
|
206
|
+
schemaVersion: 'ops-evidence-hold.aiwg.io/v1', eventId: randomUUID(),
|
|
207
|
+
artifactId: input.record.metadata.artifactId, holdId: input.holdId,
|
|
208
|
+
action: 'released', actor: input.actor, reasonDigest, occurredAt,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function receipt(record, action, outcome, occurredAt, extras = {}) {
|
|
213
|
+
return {
|
|
214
|
+
schemaVersion: 'ops-disposition-receipt.aiwg.io/v1',
|
|
215
|
+
receiptId: randomUUID(),
|
|
216
|
+
artifactId: record.metadata.artifactId,
|
|
217
|
+
policyId: record.metadata.policyId,
|
|
218
|
+
policyVersion: record.metadata.policyVersion,
|
|
219
|
+
action,
|
|
220
|
+
outcome,
|
|
221
|
+
occurredAt,
|
|
222
|
+
...(extras.destinationId ? { destinationId: extras.destinationId } : {}),
|
|
223
|
+
...(extras.errorCode ? { errorCode: extras.errorCode } : {}),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
function errorCode(error) {
|
|
227
|
+
if (error && typeof error === 'object' && 'code' in error && typeof error.code === 'string' && /^[A-Z0-9_-]{1,64}$/.test(error.code)) {
|
|
228
|
+
return error.code;
|
|
229
|
+
}
|
|
230
|
+
return 'LIFECYCLE_ACTION_FAILED';
|
|
231
|
+
}
|
|
232
|
+
/** Execute a due lifecycle action. Receipts contain identifiers/outcomes only, never removed payloads or error messages. */
|
|
233
|
+
export async function executeLifecycle(record, adapter, now = new Date()) {
|
|
234
|
+
const occurredAt = now.toISOString();
|
|
235
|
+
if (record.metadata.holds.some((hold) => !hold.releasedAt)) {
|
|
236
|
+
return receipt(record, record.metadata.action, 'held', occurredAt);
|
|
237
|
+
}
|
|
238
|
+
if (record.metadata.dispositionDeadline === null || Date.parse(record.metadata.dispositionDeadline) > now.getTime()) {
|
|
239
|
+
return receipt(record, record.metadata.action, 'not-due', occurredAt);
|
|
240
|
+
}
|
|
241
|
+
const action = record.metadata.action;
|
|
242
|
+
try {
|
|
243
|
+
switch (action) {
|
|
244
|
+
case 'retain': break;
|
|
245
|
+
case 'summarize':
|
|
246
|
+
if (!adapter.summarize)
|
|
247
|
+
throw Object.assign(new Error('summarize adapter is unavailable'), { code: 'SUMMARIZE_UNAVAILABLE' });
|
|
248
|
+
await adapter.summarize(record);
|
|
249
|
+
break;
|
|
250
|
+
case 'redact-fields':
|
|
251
|
+
if (!adapter.redactFields)
|
|
252
|
+
throw Object.assign(new Error('redact-fields adapter is unavailable'), { code: 'REDACT_FIELDS_UNAVAILABLE' });
|
|
253
|
+
await adapter.redactFields(record, record.metadata.dispositionFields ?? []);
|
|
254
|
+
break;
|
|
255
|
+
case 'archive': {
|
|
256
|
+
if (!adapter.archive)
|
|
257
|
+
throw Object.assign(new Error('archive adapter is unavailable'), { code: 'ARCHIVE_UNAVAILABLE' });
|
|
258
|
+
const destination = record.metadata.archiveSink ?? 'project-configured-archive';
|
|
259
|
+
await adapter.archive(record, destination);
|
|
260
|
+
return receipt(record, action, 'completed', occurredAt, { destinationId: destination });
|
|
261
|
+
}
|
|
262
|
+
case 'delete':
|
|
263
|
+
if (!adapter.delete)
|
|
264
|
+
throw Object.assign(new Error('delete adapter is unavailable'), { code: 'DELETE_UNAVAILABLE' });
|
|
265
|
+
await adapter.delete(record);
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
return receipt(record, action, 'completed', occurredAt);
|
|
269
|
+
}
|
|
270
|
+
catch (error) {
|
|
271
|
+
return receipt(record, action, 'failed', occurredAt, { errorCode: errorCode(error) });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=retention.js.map
|
|
@@ -2,13 +2,12 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { promises as fs } from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { resolveWorkspaceFile } from './flow.js';
|
|
5
|
+
import { redactText } from '../governance/redaction.js';
|
|
5
6
|
function redact(text, sensitiveValues) {
|
|
6
7
|
let output = text;
|
|
7
8
|
for (const value of sensitiveValues.filter(value => value.length >= 4))
|
|
8
9
|
output = output.split(value).join('[REDACTED]');
|
|
9
|
-
return output
|
|
10
|
-
.replace(/\b(authorization|cookie|set-cookie)\s*[:=]\s*[^\s,;]+/giu, '$1=[REDACTED]')
|
|
11
|
-
.replace(/\b(bearer|token)\s+[A-Za-z0-9._~+\/-]{8,}/giu, '$1 [REDACTED]');
|
|
10
|
+
return redactText(output).text;
|
|
12
11
|
}
|
|
13
12
|
async function sensitiveValues(files) {
|
|
14
13
|
const values = [];
|
package/dist/src/ops/cli.js
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
* @implements #544
|
|
12
12
|
*/
|
|
13
13
|
import { OpsRegistry } from './registry.js';
|
|
14
|
+
import { appendFile, chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { dirname, resolve } from 'node:path';
|
|
16
|
+
import { randomUUID } from 'node:crypto';
|
|
17
|
+
import { parse as parseYaml } from 'yaml';
|
|
18
|
+
import { prepareEvidenceForSink, } from '../governance/index.js';
|
|
14
19
|
/**
|
|
15
20
|
* Main CLI entry point for `aiwg ops <subcommand> [args]`
|
|
16
21
|
*/
|
|
@@ -53,6 +58,9 @@ export async function main(args) {
|
|
|
53
58
|
case 'adopt':
|
|
54
59
|
await handleAdopt(registry, subArgs);
|
|
55
60
|
break;
|
|
61
|
+
case 'evidence':
|
|
62
|
+
await handleEvidence(subArgs);
|
|
63
|
+
break;
|
|
56
64
|
default:
|
|
57
65
|
printUsage();
|
|
58
66
|
if (subcommand) {
|
|
@@ -61,6 +69,91 @@ export async function main(args) {
|
|
|
61
69
|
break;
|
|
62
70
|
}
|
|
63
71
|
}
|
|
72
|
+
function flagValue(args, name) {
|
|
73
|
+
const index = args.indexOf(name);
|
|
74
|
+
if (index === -1)
|
|
75
|
+
return undefined;
|
|
76
|
+
const value = args[index + 1];
|
|
77
|
+
if (!value || value.startsWith('--'))
|
|
78
|
+
throw new Error(`${name} requires a value`);
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function parseData(source, label) {
|
|
82
|
+
try {
|
|
83
|
+
return JSON.parse(source);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
try {
|
|
87
|
+
return parseYaml(source);
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new Error(`${label} must contain valid JSON or YAML`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async function readStdin(maxBytes = 16 * 1024 * 1024) {
|
|
95
|
+
const chunks = [];
|
|
96
|
+
let bytes = 0;
|
|
97
|
+
for await (const chunk of process.stdin) {
|
|
98
|
+
const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
99
|
+
bytes += value.length;
|
|
100
|
+
if (bytes > maxBytes)
|
|
101
|
+
throw new Error('evidence input exceeds 16 MiB');
|
|
102
|
+
chunks.push(value);
|
|
103
|
+
}
|
|
104
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
105
|
+
}
|
|
106
|
+
async function readInput(args) {
|
|
107
|
+
const inputPath = flagValue(args, '--input');
|
|
108
|
+
return inputPath ? readFile(resolve(inputPath), 'utf8') : readStdin();
|
|
109
|
+
}
|
|
110
|
+
async function appendBoundaryAudit(cwd, value, configuredPath) {
|
|
111
|
+
const path = resolve(configuredPath ?? `${cwd}/.aiwg/ops/audit/governance-boundary.jsonl`);
|
|
112
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
113
|
+
await appendFile(path, `${JSON.stringify(value)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
114
|
+
await chmod(path, 0o600);
|
|
115
|
+
}
|
|
116
|
+
async function writePreparedOutput(pathValue, value) {
|
|
117
|
+
const path = resolve(pathValue);
|
|
118
|
+
const temporary = `${path}.tmp-${randomUUID()}`;
|
|
119
|
+
await mkdir(dirname(path), { recursive: true, mode: 0o700 });
|
|
120
|
+
await writeFile(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
121
|
+
await rename(temporary, path);
|
|
122
|
+
await chmod(path, 0o600);
|
|
123
|
+
}
|
|
124
|
+
async function handleEvidence(args) {
|
|
125
|
+
const action = args[0];
|
|
126
|
+
if (action !== 'prepare') {
|
|
127
|
+
throw new Error('Usage: aiwg ops evidence prepare [--input <json-or-yaml>] [--policy <json-or-yaml>] [--output <path>] [--audit <path>]');
|
|
128
|
+
}
|
|
129
|
+
const envelope = parseData(await readInput(args), 'evidence input');
|
|
130
|
+
if (!envelope || typeof envelope !== 'object' || !envelope.artifact || typeof envelope.sinkId !== 'string') {
|
|
131
|
+
throw new Error('evidence input requires artifact and sinkId');
|
|
132
|
+
}
|
|
133
|
+
const policyPath = flagValue(args, '--policy');
|
|
134
|
+
const policy = policyPath
|
|
135
|
+
? parseData(await readFile(resolve(policyPath), 'utf8'), 'governance policy')
|
|
136
|
+
: undefined;
|
|
137
|
+
const result = prepareEvidenceForSink({
|
|
138
|
+
...envelope,
|
|
139
|
+
policy,
|
|
140
|
+
});
|
|
141
|
+
await appendBoundaryAudit(process.cwd(), result.audit, flagValue(args, '--audit'));
|
|
142
|
+
const response = result.allowed && result.prepared
|
|
143
|
+
? { schemaVersion: 'ops-prepared-evidence.aiwg.io/v1', prepared: result.prepared, audit: result.audit, auditRecorded: true }
|
|
144
|
+
: { schemaVersion: 'ops-prepared-evidence.aiwg.io/v1', denied: true, audit: result.audit, auditRecorded: true };
|
|
145
|
+
const outputPath = flagValue(args, '--output');
|
|
146
|
+
if (outputPath) {
|
|
147
|
+
if (!result.allowed)
|
|
148
|
+
throw new Error(`evidence publication denied: ${result.audit.reasonCodes.join(', ')}`);
|
|
149
|
+
await writePreparedOutput(outputPath, response);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
process.stdout.write(`${JSON.stringify(response, null, 2)}\n`);
|
|
153
|
+
}
|
|
154
|
+
if (!result.allowed)
|
|
155
|
+
process.exitCode = 2;
|
|
156
|
+
}
|
|
64
157
|
async function handleInit(registry, args) {
|
|
65
158
|
// Parse flags
|
|
66
159
|
let silent = false;
|
|
@@ -225,6 +318,7 @@ Subcommands:
|
|
|
225
318
|
push [--workspace <n>] Push workspace repos to remote
|
|
226
319
|
discover [root...] Scan filesystem for orphaned ops-workspace clones
|
|
227
320
|
adopt <path> Register an existing local clone as a repo entry
|
|
321
|
+
evidence prepare Sanitize and policy-gate evidence before publication
|
|
228
322
|
|
|
229
323
|
Init options:
|
|
230
324
|
--silent Skip interactive prompts
|
|
@@ -251,6 +345,7 @@ Examples:
|
|
|
251
345
|
aiwg ops push --workspace personal
|
|
252
346
|
aiwg ops discover ~/projects ~/work --max-depth 4
|
|
253
347
|
aiwg ops discover --register --workspace home
|
|
348
|
+
aiwg ops evidence prepare --input request.yaml --policy .aiwg/ops/governance-policy.yaml
|
|
254
349
|
|
|
255
350
|
Discover options:
|
|
256
351
|
--max-depth <n> Max walk depth from each root (default: 3)
|
|
@@ -85,7 +85,7 @@ async function dispatchV2(executor, payload, opts) {
|
|
|
85
85
|
// discovery dependency to the compatibility path.
|
|
86
86
|
if (clientOpts.protocolPolicy === '0.3') {
|
|
87
87
|
clientOpts.selectedInterface = {
|
|
88
|
-
url: `${executor.transportEndpoints.rest.replace(/\/+$/, '')}/agents/${encodeURIComponent(a2aInstanceId)}`,
|
|
88
|
+
url: `${executor.transportEndpoints.rest.replace(/\/+$/, '')}/agents/${encodeURIComponent(a2aInstanceId)}/v1`,
|
|
89
89
|
protocolBinding: 'REST',
|
|
90
90
|
protocolVersion: '0.3',
|
|
91
91
|
preference: 0,
|
|
@@ -1,21 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { requireFeaturePackage } from '../features/runtime.js';
|
|
2
2
|
import { CandidateReviewReceiptSchema, DeletionReceiptSchema, IntelligenceCandidateSchema, PromotionDependencyDecisionSchema, PromotionReceiptSchema, SessionContractError, sha256, } from './contracts.js';
|
|
3
3
|
import { coverageFromBatchRun, } from './batch-contracts.js';
|
|
4
4
|
import { redactSessionText, sanitizeNativeExtensions } from './policy.js';
|
|
5
5
|
import { deriveSessionAnalytics, } from './analytics.js';
|
|
6
|
-
const require = createRequire(import.meta.url);
|
|
7
6
|
const POLICY_PROVIDER_MIGRATION = 'policy-provider-identity:v2';
|
|
8
7
|
const EVENT_ORIGIN_INTENT_MIGRATION = 'event-origin-intent:v1';
|
|
9
8
|
export class SessionRepository {
|
|
10
9
|
db;
|
|
11
10
|
constructor(path = ':memory:') {
|
|
11
|
+
let Database;
|
|
12
12
|
try {
|
|
13
|
-
|
|
14
|
-
this.db = new Database(path);
|
|
13
|
+
Database = requireFeaturePackage('better-sqlite3');
|
|
15
14
|
}
|
|
16
|
-
catch {
|
|
17
|
-
throw new Error('session SQLite
|
|
15
|
+
catch (cause) {
|
|
16
|
+
throw new Error('session SQLite catalog is unavailable because better-sqlite3 could not be loaded; ' +
|
|
17
|
+
'run `aiwg features install sqlite`, then retry. If the native build fails, install ' +
|
|
18
|
+
'Python 3, make, and a C/C++ compiler supported by node-gyp.', { cause });
|
|
18
19
|
}
|
|
20
|
+
this.db = new Database(path);
|
|
19
21
|
this.db.pragma('foreign_keys = ON');
|
|
20
22
|
this.db.pragma('journal_mode = WAL');
|
|
21
23
|
this.db.pragma('busy_timeout = 5000');
|