@aiwg/cli 2026.8.27 → 2026.9.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/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/cli/handlers/use.js +40 -7
- 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 +2 -1
package/bin/aiwg.mjs
CHANGED
|
@@ -407,6 +407,12 @@ async function main() {
|
|
|
407
407
|
// commands remain reachable so an operator can explicitly adopt or switch.
|
|
408
408
|
if (args[0] !== 'installation') {
|
|
409
409
|
const identityPath = path.join(activePackageRoot, 'dist', 'src', 'installation', 'manager.mjs');
|
|
410
|
+
if (activePackageRoot !== packageRoot && !existsSync(identityPath)) {
|
|
411
|
+
console.error(`Dev mode: compiled installation manager not found at ${identityPath}`);
|
|
412
|
+
console.error(` Run: (cd ${activePackageRoot} && npm run build:cli)`);
|
|
413
|
+
console.error(` Or switch back: aiwg --use-stable`);
|
|
414
|
+
process.exit(1);
|
|
415
|
+
}
|
|
410
416
|
const { assertCanonicalInstallation } = await import(pathToFileURL(identityPath).href);
|
|
411
417
|
assertCanonicalInstallation({ actualRoot: activePackageRoot });
|
|
412
418
|
}
|
package/dist/src/a2a/client.js
CHANGED
|
@@ -102,7 +102,10 @@ export class A2AClient {
|
|
|
102
102
|
}
|
|
103
103
|
operationPath(v1Path, legacyPath) {
|
|
104
104
|
if (this.selectedInterface) {
|
|
105
|
-
|
|
105
|
+
// An advertised interface URL is already the operation base. Appending
|
|
106
|
+
// the legacy route prefix again turns cards ending in `/v1` into
|
|
107
|
+
// `/v1/v1/...` and breaks otherwise valid negotiated 0.3 calls.
|
|
108
|
+
return this.protocolVersion === '1.0' ? `/${v1Path}` : `/${legacyPath}`;
|
|
106
109
|
}
|
|
107
110
|
return `${this.agentPath()}/${legacyPath}`;
|
|
108
111
|
}
|
package/dist/src/a2a/codecs.js
CHANGED
|
@@ -24,8 +24,11 @@ export function decodePushNotificationConfig(version, input, path = '$') {
|
|
|
24
24
|
const result = {
|
|
25
25
|
url: stringAt(version, `${path}.url`, obj.url),
|
|
26
26
|
};
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// The deployed 0.3 sandbox predates the stable field rename and returns
|
|
28
|
+
// `id`; accept that compatibility spelling while continuing to encode the
|
|
29
|
+
// documented 0.3 `configId` shape.
|
|
30
|
+
const id = version === '1.0' ? obj.id : (obj.configId ?? obj.id);
|
|
31
|
+
assignOptionalString(version, result, 'configId', id, `${path}.${version === '1.0' || obj.configId === undefined ? 'id' : 'configId'}`);
|
|
29
32
|
assignOptionalString(version, result, 'token', obj.token, `${path}.token`);
|
|
30
33
|
if (version === '0.3') {
|
|
31
34
|
assignOptionalString(version, result, 'secret', obj.secret, `${path}.secret`);
|
package/dist/src/a2a/protocol.js
CHANGED
|
@@ -38,7 +38,7 @@ export function normalizeAgentCard(card) {
|
|
|
38
38
|
if (!entry || typeof entry !== 'object' || typeof entry.url !== 'string') {
|
|
39
39
|
throw new A2ANegotiationError('agent_card.interface_invalid', `supportedInterfaces[${preference}] must contain an absolute URL`);
|
|
40
40
|
}
|
|
41
|
-
|
|
41
|
+
assertAbsoluteInterfaceUrl(entry.url, `supportedInterfaces[${preference}].url`);
|
|
42
42
|
const interfaceVersion = normalizeProtocolVersion(entry.protocolVersion);
|
|
43
43
|
if (entry.protocolVersion !== undefined && !interfaceVersion) {
|
|
44
44
|
throw new A2ANegotiationError('agent_card.interface_version_invalid', `supportedInterfaces[${preference}].protocolVersion is unsupported`);
|
|
@@ -133,4 +133,15 @@ function assertAbsoluteUrl(value, field) {
|
|
|
133
133
|
throw new A2ANegotiationError('agent_card.url_invalid', `${field} must be an absolute HTTP(S) URL`);
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
|
+
function assertAbsoluteInterfaceUrl(value, field) {
|
|
137
|
+
try {
|
|
138
|
+
const url = new URL(value);
|
|
139
|
+
if (!['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) {
|
|
140
|
+
throw new Error('unsupported interface scheme');
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
throw new A2ANegotiationError('agent_card.url_invalid', `${field} must be an absolute HTTP(S) or WS(S) URL`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
136
147
|
//# sourceMappingURL=protocol.js.map
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { ACTIVITY_OPERATIONS, formatEntry, formatUtcTimestamp, isActivityOperation, } from './types.js';
|
|
19
19
|
import { parseLog, parseUtcDate } from './parser.js';
|
|
20
20
|
import { resolveStorage } from '../storage/index.js';
|
|
21
|
+
import { redactText } from '../governance/redaction.js';
|
|
21
22
|
const LOG_PATH = 'activity.log';
|
|
22
23
|
const DEFAULT_LIMIT = 20;
|
|
23
24
|
export async function main(args) {
|
|
@@ -74,7 +75,9 @@ async function handleAppend(args) {
|
|
|
74
75
|
` Valid operations: ${ACTIVITY_OPERATIONS.join(', ')}`);
|
|
75
76
|
}
|
|
76
77
|
const op = args[0];
|
|
77
|
-
const summary = args.slice(1).join(' ').trim()
|
|
78
|
+
const summary = redactText(args.slice(1).join(' ').trim(), {
|
|
79
|
+
limits: { maxInputBytes: 16 * 1024 },
|
|
80
|
+
}).text;
|
|
78
81
|
if (!isActivityOperation(op)) {
|
|
79
82
|
throw new Error(`Invalid operation "${op}". Valid operations: ${ACTIVITY_OPERATIONS.join(', ')}`);
|
|
80
83
|
}
|
package/dist/src/api/index.d.ts
CHANGED
|
@@ -18,5 +18,6 @@ export * from '../providers/transformation-receipt-integration.js';
|
|
|
18
18
|
export * from '../marketplace/artifact-attestation.js';
|
|
19
19
|
export * from '../uhp/index.js';
|
|
20
20
|
export * from '../mission-protocol/index.js';
|
|
21
|
+
export * from '../governance/index.js';
|
|
21
22
|
export { ARTIFACT_TRUST_ROOT_MEDIA_TYPE, ARTIFACT_TRUST_ROOT_SCHEMA_VERSION, ARTIFACT_TRUST_STATE_SCHEMA_VERSION, bootstrapTrustRoot, parseTrustRoot, parseTrustState, readTrustState, validateTrustRoot, validateTrustState, verifyRootTransition, writeTrustState, channelStateKey, canonicalJson, dssePae, publicKeyFingerprint, sha256, type ArtifactTrustRoot, type ArtifactTrustState, type RootBootstrapResult, type RootTransitionResult, type ArtifactTrustPolicySettings, type TrustedChannelState, } from '../security/artifact-trust.js';
|
|
22
23
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/src/api/index.js
CHANGED
|
@@ -18,5 +18,6 @@ export * from '../providers/transformation-receipt-integration.js';
|
|
|
18
18
|
export * from '../marketplace/artifact-attestation.js';
|
|
19
19
|
export * from '../uhp/index.js';
|
|
20
20
|
export * from '../mission-protocol/index.js';
|
|
21
|
+
export * from '../governance/index.js';
|
|
21
22
|
export { ARTIFACT_TRUST_ROOT_MEDIA_TYPE, ARTIFACT_TRUST_ROOT_SCHEMA_VERSION, ARTIFACT_TRUST_STATE_SCHEMA_VERSION, bootstrapTrustRoot, parseTrustRoot, parseTrustState, readTrustState, validateTrustRoot, validateTrustState, verifyRootTransition, writeTrustState, channelStateKey, canonicalJson, dssePae, publicKeyFingerprint, sha256, } from '../security/artifact-trust.js';
|
|
22
23
|
//# sourceMappingURL=index.js.map
|
|
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { parse as parseYaml } from "yaml";
|
|
5
6
|
import { GRAPH_CONFIGS, getProjectIndexRoot, loadGlobalGraphConfigs } from "./types.js";
|
|
6
7
|
import { buildAiwgFortemiIndexExport, } from "./browser-export.js";
|
|
7
8
|
import { loadGraphIndexFile } from "./index-reader.js";
|
|
@@ -263,4 +264,40 @@ export function getFortemiCorePrebuiltStatus(graph = "framework") {
|
|
|
263
264
|
reason: !exportExists ? "prebuilt manifest exists but export file is missing" : reason,
|
|
264
265
|
};
|
|
265
266
|
}
|
|
267
|
+
/**
|
|
268
|
+
* Compare source script declarations with their compact prebuilt records.
|
|
269
|
+
* This is deliberately independent of cache freshness: a checksum-valid index
|
|
270
|
+
* can still be operationally broken when compaction drops runtime metadata.
|
|
271
|
+
*/
|
|
272
|
+
export function getFortemiCoreExecutableSkillStatus(graph = "framework", packageRoot) {
|
|
273
|
+
const root = packageRoot ?? findPackageRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
274
|
+
if (!root)
|
|
275
|
+
return { sourceExecutableCount: 0, packagedExecutableCount: 0, missing: [] };
|
|
276
|
+
const exportPath = path.join(root, "prebuilt", "fortemi-core", graph, "aiwg-fortemi-index-v2.json");
|
|
277
|
+
if (!fs.existsSync(exportPath))
|
|
278
|
+
return { sourceExecutableCount: 0, packagedExecutableCount: 0, missing: [] };
|
|
279
|
+
const exported = JSON.parse(fs.readFileSync(exportPath, "utf-8"));
|
|
280
|
+
let sourceExecutableCount = 0;
|
|
281
|
+
let packagedExecutableCount = 0;
|
|
282
|
+
const missing = [];
|
|
283
|
+
for (const item of exported.items ?? []) {
|
|
284
|
+
if (item.type !== "aiwg.skill" || typeof item.source?.path !== "string")
|
|
285
|
+
continue;
|
|
286
|
+
const sourcePath = path.join(root, item.source.path);
|
|
287
|
+
if (!fs.existsSync(sourcePath))
|
|
288
|
+
continue;
|
|
289
|
+
const match = fs.readFileSync(sourcePath, "utf-8").match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
|
|
290
|
+
if (!match)
|
|
291
|
+
continue;
|
|
292
|
+
const script = parseYaml(match[1])?.script;
|
|
293
|
+
if (!script)
|
|
294
|
+
continue;
|
|
295
|
+
sourceExecutableCount += 1;
|
|
296
|
+
if (item.search?.frontmatter?.aiwg_script)
|
|
297
|
+
packagedExecutableCount += 1;
|
|
298
|
+
else
|
|
299
|
+
missing.push(item.name ?? item.id);
|
|
300
|
+
}
|
|
301
|
+
return { sourceExecutableCount, packagedExecutableCount, missing };
|
|
302
|
+
}
|
|
266
303
|
//# sourceMappingURL=fortemi-core-sync.js.map
|
|
@@ -10,9 +10,8 @@
|
|
|
10
10
|
import { createHash, randomUUID } from 'node:crypto';
|
|
11
11
|
import { appendFile, chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
12
12
|
import { dirname } from 'node:path';
|
|
13
|
+
import { redactStructured } from '../governance/redaction.js';
|
|
13
14
|
export const OPERATOR_DECISION_SCHEMA = 'operator-decision.aiwg.io/v1';
|
|
14
|
-
const secretKey = /token|secret|password|credential|authorization|cookie|csrf|api[_-]?key/i;
|
|
15
|
-
const secretValue = /(?:bearer\s+\S+|\bsk-[a-z0-9_-]+|\bgh[pousr]_[a-z0-9_]+)/i;
|
|
16
15
|
export function digestDecisionContext(context) {
|
|
17
16
|
const safe = redact(context).value;
|
|
18
17
|
return `sha256:${createHash('sha256').update(canonicalJson(safe)).digest('hex')}`;
|
|
@@ -167,28 +166,13 @@ function canonicalJson(value) {
|
|
|
167
166
|
return JSON.stringify(value);
|
|
168
167
|
}
|
|
169
168
|
function redact(value, path = '$') {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (secretKey.test(key)) {
|
|
179
|
-
output[key] = '[redacted]';
|
|
180
|
-
paths.push(`${path}.${key}`);
|
|
181
|
-
}
|
|
182
|
-
else {
|
|
183
|
-
const child = redact(item, `${path}.${key}`);
|
|
184
|
-
output[key] = child.value;
|
|
185
|
-
paths.push(...child.paths);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return { value: output, paths };
|
|
189
|
-
}
|
|
190
|
-
if (typeof value === 'string' && secretValue.test(value))
|
|
191
|
-
return { value: '[redacted]', paths: [path] };
|
|
192
|
-
return { value, paths: [] };
|
|
169
|
+
const result = redactStructured(value);
|
|
170
|
+
return {
|
|
171
|
+
value: result.value,
|
|
172
|
+
paths: result.findings.map((finding) => {
|
|
173
|
+
const suffix = finding.path?.replaceAll('/', '.').replace(/^\./, '') ?? '';
|
|
174
|
+
return suffix ? `${path}.${suffix}` : path;
|
|
175
|
+
}),
|
|
176
|
+
};
|
|
193
177
|
}
|
|
194
178
|
//# sourceMappingURL=operator-decision.js.map
|
|
@@ -1022,12 +1022,13 @@ async function countBundleDeployedArtifacts(bundlePath, target, provider) {
|
|
|
1022
1022
|
}
|
|
1023
1023
|
const SKILL_SUPPORT_REFERENCE = /(?:^|[\s`('"\[])((?:templates|references|scripts|assets)\/[A-Za-z0-9._@/+\-]+)(?=$|[\s`)'"\],:;])/gm;
|
|
1024
1024
|
/**
|
|
1025
|
-
*
|
|
1026
|
-
*
|
|
1025
|
+
* Skill-relative support files may live beside the skill or at the bundle root
|
|
1026
|
+
* (plugin payloads commonly share report templates). Materialize
|
|
1027
1027
|
* only paths explicitly named by SKILL.md, and fail closed on missing or
|
|
1028
1028
|
* unsafe sources so a deployed instruction can never point at absent assets.
|
|
1029
1029
|
*/
|
|
1030
|
-
async function
|
|
1030
|
+
async function reconcileDeployedSkillAssets(bundlePath, target, provider, options = {}) {
|
|
1031
|
+
const strictReferences = options.strictReferences ?? true;
|
|
1031
1032
|
const skillsRoot = path.join(bundlePath, 'skills');
|
|
1032
1033
|
let skillDirs;
|
|
1033
1034
|
try {
|
|
@@ -1054,7 +1055,13 @@ async function reconcileProjectLocalSkillAssets(bundlePath, target, provider) {
|
|
|
1054
1055
|
catch {
|
|
1055
1056
|
continue;
|
|
1056
1057
|
}
|
|
1057
|
-
const
|
|
1058
|
+
const frontmatter = content.match(/^---\r?\n([\s\S]*?)\r?\n---/)?.[1] ?? '';
|
|
1059
|
+
const declaredEntrypoint = frontmatter
|
|
1060
|
+
.match(/^[ \t]+entrypoint:\s*["']?([^"'\s]+)["']?\s*$/m)?.[1];
|
|
1061
|
+
const declaredEntrypoints = new Set(declaredEntrypoint ? [declaredEntrypoint] : []);
|
|
1062
|
+
const references = [...new Set([
|
|
1063
|
+
...content.matchAll(SKILL_SUPPORT_REFERENCE),
|
|
1064
|
+
].map(match => match[1]).concat([...declaredEntrypoints]))];
|
|
1058
1065
|
for (const relative of references) {
|
|
1059
1066
|
const normalized = path.posix.normalize(relative);
|
|
1060
1067
|
if (normalized !== relative || normalized.startsWith('../') || path.isAbsolute(normalized)) {
|
|
@@ -1072,8 +1079,12 @@ async function reconcileProjectLocalSkillAssets(bundlePath, target, provider) {
|
|
|
1072
1079
|
}
|
|
1073
1080
|
catch { /* try bundle-root fallback */ }
|
|
1074
1081
|
}
|
|
1075
|
-
if (!source)
|
|
1076
|
-
|
|
1082
|
+
if (!source) {
|
|
1083
|
+
if (strictReferences || declaredEntrypoints.has(relative)) {
|
|
1084
|
+
throw new Error(`missing skill support asset '${relative}' referenced by ${sourceSkillMd}`);
|
|
1085
|
+
}
|
|
1086
|
+
continue;
|
|
1087
|
+
}
|
|
1077
1088
|
let deployedSkillRoot;
|
|
1078
1089
|
for (const root of deployRoots) {
|
|
1079
1090
|
// The deployer may select the bulk or kernel tier; use the tier that
|
|
@@ -1162,7 +1173,7 @@ async function deployOneProjectLocalBundle(opts) {
|
|
|
1162
1173
|
exitCode = result.exitCode;
|
|
1163
1174
|
if (exitCode === 0 && !dryRun) {
|
|
1164
1175
|
try {
|
|
1165
|
-
await
|
|
1176
|
+
await reconcileDeployedSkillAssets(bundle.artifactPath, target, provider);
|
|
1166
1177
|
}
|
|
1167
1178
|
catch (error) {
|
|
1168
1179
|
ui.warn(`Project-local skill asset deployment failed for '${bundle.id}': ${error.message}`);
|
|
@@ -2733,6 +2744,17 @@ export class UseHandler {
|
|
|
2733
2744
|
|| `Failed to deploy required addon '${dependency}'`,
|
|
2734
2745
|
};
|
|
2735
2746
|
}
|
|
2747
|
+
if (!dryRunAddon) {
|
|
2748
|
+
try {
|
|
2749
|
+
await reconcileDeployedSkillAssets(dependencySource, target, provider, { strictReferences: false });
|
|
2750
|
+
}
|
|
2751
|
+
catch (error) {
|
|
2752
|
+
return {
|
|
2753
|
+
exitCode: 1,
|
|
2754
|
+
message: `Required addon '${dependency}' skill asset deployment failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2736
2758
|
try {
|
|
2737
2759
|
await registerSourceCliCommands({
|
|
2738
2760
|
source: dependencySource,
|
|
@@ -2764,6 +2786,17 @@ export class UseHandler {
|
|
|
2764
2786
|
if (addonResult.exitCode !== 0) {
|
|
2765
2787
|
return addonResult;
|
|
2766
2788
|
}
|
|
2789
|
+
if (!dryRunAddon) {
|
|
2790
|
+
try {
|
|
2791
|
+
await reconcileDeployedSkillAssets(addonSource, target, provider, { strictReferences: false });
|
|
2792
|
+
}
|
|
2793
|
+
catch (error) {
|
|
2794
|
+
return {
|
|
2795
|
+
exitCode: 1,
|
|
2796
|
+
message: `${kind} '${framework}' skill asset deployment failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2767
2800
|
// Register only artifacts actually written by a confirmed deployment.
|
|
2768
2801
|
if (!dryRunAddon) {
|
|
2769
2802
|
try {
|
|
@@ -32,15 +32,16 @@ export const FEATURE_CATALOG = [
|
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
34
|
name: 'sqlite',
|
|
35
|
-
description: 'SQLite
|
|
35
|
+
description: 'SQLite runtime for session catalogs and persistent storage backends',
|
|
36
36
|
packages: ['better-sqlite3'],
|
|
37
37
|
packageSpecs: { 'better-sqlite3': '12.8.0' },
|
|
38
38
|
scriptPackages: ['better-sqlite3'],
|
|
39
39
|
enables: [
|
|
40
|
+
'aiwg sessions list / discover / import-discovered / timeline / search',
|
|
40
41
|
'storage.config: backend=sqlite for any subsystem',
|
|
41
42
|
'transactional reads/writes against `.aiwg/storage/`',
|
|
42
43
|
],
|
|
43
|
-
cost: '~5 MB — native compile via node-gyp',
|
|
44
|
+
cost: '~5 MB — platform prebuild when available, otherwise a native compile via node-gyp',
|
|
44
45
|
},
|
|
45
46
|
{
|
|
46
47
|
name: 'postgres',
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { createSanitizedSummary, evaluatePublicationGate, resolveArtifactGovernance, resolveClassificationDefinitions, } from './classification.js';
|
|
3
|
+
import { createEvidenceLifecycle, validateRetentionRules, } from './retention.js';
|
|
4
|
+
import { redactStructured, redactText, } from './redaction.js';
|
|
5
|
+
export const DEFAULT_PUBLICATION_SINKS = {
|
|
6
|
+
'local-ephemeral': {
|
|
7
|
+
id: 'local-ephemeral', visibility: 'restricted', external: false,
|
|
8
|
+
persistent: false, mutable: true, maxClassification: 'restricted-identity',
|
|
9
|
+
},
|
|
10
|
+
'private-repository': {
|
|
11
|
+
id: 'private-repository', visibility: 'private', external: false,
|
|
12
|
+
persistent: true, mutable: true, maxClassification: 'restricted-infrastructure',
|
|
13
|
+
},
|
|
14
|
+
'public-repository': {
|
|
15
|
+
id: 'public-repository', visibility: 'public', external: true,
|
|
16
|
+
persistent: true, mutable: true, maxClassification: 'public', acceptsSanitizedSummary: true,
|
|
17
|
+
},
|
|
18
|
+
'private-issue': {
|
|
19
|
+
id: 'private-issue', visibility: 'private', external: true,
|
|
20
|
+
persistent: true, mutable: false, maxClassification: 'confidential', acceptsSanitizedSummary: true,
|
|
21
|
+
},
|
|
22
|
+
'public-issue': {
|
|
23
|
+
id: 'public-issue', visibility: 'public', external: true,
|
|
24
|
+
persistent: true, mutable: false, maxClassification: 'public', acceptsSanitizedSummary: true,
|
|
25
|
+
},
|
|
26
|
+
'encrypted-artifact-store': {
|
|
27
|
+
id: 'encrypted-artifact-store', visibility: 'restricted', external: false,
|
|
28
|
+
persistent: true, mutable: true, maxClassification: 'restricted-identity',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
export function resolveGovernancePolicy(policy = {}) {
|
|
32
|
+
const resolved = {
|
|
33
|
+
...policy,
|
|
34
|
+
sinks: { ...DEFAULT_PUBLICATION_SINKS, ...(policy.sinks ?? {}) },
|
|
35
|
+
};
|
|
36
|
+
const classes = resolveClassificationDefinitions(resolved.classification);
|
|
37
|
+
const classificationReferences = [
|
|
38
|
+
resolved.classification?.defaultClassification,
|
|
39
|
+
...Object.values(resolved.classification?.defaultsByKind ?? {}),
|
|
40
|
+
...Object.values(resolved.classification?.defaultsByCategory ?? {}),
|
|
41
|
+
].filter((value) => value !== undefined);
|
|
42
|
+
for (const value of classificationReferences) {
|
|
43
|
+
if (!classes[value])
|
|
44
|
+
throw new Error(`governance policy references unknown classification '${value}'`);
|
|
45
|
+
}
|
|
46
|
+
for (const [id, sink] of Object.entries(resolved.sinks ?? {})) {
|
|
47
|
+
if (!sink || typeof sink !== 'object')
|
|
48
|
+
throw new Error(`sink '${id}' must be an object`);
|
|
49
|
+
if (sink.id !== id)
|
|
50
|
+
throw new Error(`sink map key '${id}' does not match sink ID '${sink.id}'`);
|
|
51
|
+
if (!new Set(['public', 'private', 'restricted', 'unknown']).has(sink.visibility)) {
|
|
52
|
+
throw new Error(`sink '${id}' has invalid visibility`);
|
|
53
|
+
}
|
|
54
|
+
for (const property of ['external', 'persistent', 'mutable']) {
|
|
55
|
+
if (typeof sink[property] !== 'boolean')
|
|
56
|
+
throw new Error(`sink '${id}' requires boolean ${property}`);
|
|
57
|
+
}
|
|
58
|
+
for (const property of ['acceptsSanitizedSummary', 'allowRedactionOverride']) {
|
|
59
|
+
if (sink[property] !== undefined && typeof sink[property] !== 'boolean') {
|
|
60
|
+
throw new Error(`sink '${id}' requires boolean ${property}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (sink.maxClassification && !classes[sink.maxClassification]) {
|
|
64
|
+
throw new Error(`sink '${id}' references unknown classification '${sink.maxClassification}'`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
validateRetentionRules(resolved.retention ?? []);
|
|
68
|
+
// Compile configured patterns up front. Empty input cannot create findings,
|
|
69
|
+
// but invalid or high-risk organization patterns still fail validation.
|
|
70
|
+
redactText('', resolved.redaction ?? {});
|
|
71
|
+
redactStructured({ validation: 'ok' }, resolved.redaction ?? {});
|
|
72
|
+
return resolved;
|
|
73
|
+
}
|
|
74
|
+
function sha256(value) {
|
|
75
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
76
|
+
}
|
|
77
|
+
function safeAuditLabel(value) {
|
|
78
|
+
try {
|
|
79
|
+
if (/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/.test(value) && redactText(value).sensitivity === 'none')
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Hashing is the fail-closed representation for malformed labels.
|
|
84
|
+
}
|
|
85
|
+
return sha256(value);
|
|
86
|
+
}
|
|
87
|
+
function stableStatus(value) {
|
|
88
|
+
if (!value)
|
|
89
|
+
return undefined;
|
|
90
|
+
const normalized = value.toLowerCase().replaceAll('_', '-').replaceAll(' ', '-');
|
|
91
|
+
return new Set([
|
|
92
|
+
'ok', 'pass', 'passed', 'fail', 'failed', 'complete', 'completed', 'blocked',
|
|
93
|
+
'partial', 'unknown', 'in-progress', 'review-needed', 'success', 'error',
|
|
94
|
+
]).has(normalized) ? normalized : undefined;
|
|
95
|
+
}
|
|
96
|
+
function contentDigest(value) {
|
|
97
|
+
if (typeof value === 'string')
|
|
98
|
+
return sha256(value);
|
|
99
|
+
try {
|
|
100
|
+
return sha256(JSON.stringify(value) ?? '[undefined]');
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return sha256('[unserializable]');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function boundedExcerpt(value, maxBytes) {
|
|
107
|
+
if (typeof value !== 'string')
|
|
108
|
+
return undefined;
|
|
109
|
+
const source = Buffer.from(value);
|
|
110
|
+
return {
|
|
111
|
+
excerpt: source.subarray(0, maxBytes).toString('utf8'),
|
|
112
|
+
bytes: source.length,
|
|
113
|
+
digest: sha256(value),
|
|
114
|
+
truncated: source.length > maxBytes,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/** Reduce command evidence to outcomes, bounded excerpts, counts, and correlation digests by default. */
|
|
118
|
+
export function minimizeEvidence(payload, maxExcerptBytes = 512) {
|
|
119
|
+
if (!Number.isSafeInteger(maxExcerptBytes) || maxExcerptBytes < 0 || maxExcerptBytes > 64 * 1024) {
|
|
120
|
+
throw new Error('maxExcerptBytes must be an integer from 0 through 65536');
|
|
121
|
+
}
|
|
122
|
+
if (typeof payload === 'string') {
|
|
123
|
+
return boundedExcerpt(payload, maxExcerptBytes);
|
|
124
|
+
}
|
|
125
|
+
if (Array.isArray(payload)) {
|
|
126
|
+
return { itemCount: payload.length, digest: contentDigest(payload) };
|
|
127
|
+
}
|
|
128
|
+
if (!payload || typeof payload !== 'object')
|
|
129
|
+
return payload;
|
|
130
|
+
const source = payload;
|
|
131
|
+
const result = {
|
|
132
|
+
schemaVersion: 'ops-minimum-evidence.aiwg.io/v1',
|
|
133
|
+
sourceFieldCount: Object.keys(source).length,
|
|
134
|
+
sourceDigest: contentDigest(source),
|
|
135
|
+
};
|
|
136
|
+
for (const key of ['status', 'outcome', 'success', 'exitCode', 'durationMs', 'startedAt', 'completedAt']) {
|
|
137
|
+
const value = source[key];
|
|
138
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean')
|
|
139
|
+
result[key] = value;
|
|
140
|
+
}
|
|
141
|
+
const stdout = boundedExcerpt(source.stdout ?? source.output, maxExcerptBytes);
|
|
142
|
+
const stderr = boundedExcerpt(source.stderr, maxExcerptBytes);
|
|
143
|
+
if (stdout)
|
|
144
|
+
result.stdout = stdout;
|
|
145
|
+
if (stderr)
|
|
146
|
+
result.stderr = stderr;
|
|
147
|
+
if (source.command !== undefined)
|
|
148
|
+
result.commandDigest = contentDigest(source.command);
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
function validOverride(override, artifactId, sinkId, now) {
|
|
152
|
+
if (!override?.id || !override.actor || !override.reason.trim())
|
|
153
|
+
return false;
|
|
154
|
+
if (override.artifactId !== artifactId || override.sinkId !== sinkId)
|
|
155
|
+
return false;
|
|
156
|
+
const approvedAt = Date.parse(override.approvedAt);
|
|
157
|
+
if (!Number.isFinite(approvedAt) || approvedAt > now)
|
|
158
|
+
return false;
|
|
159
|
+
if (override.expiresAt !== undefined) {
|
|
160
|
+
const expiresAt = Date.parse(override.expiresAt);
|
|
161
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= now || expiresAt <= approvedAt)
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
function emptyAudit(input, sinkId, now) {
|
|
167
|
+
return {
|
|
168
|
+
schemaVersion: 'ops-evidence-boundary.aiwg.io/v1',
|
|
169
|
+
eventId: randomUUID(),
|
|
170
|
+
occurredAt: now.toISOString(),
|
|
171
|
+
artifactId: sha256(input.id),
|
|
172
|
+
artifactKind: safeAuditLabel(input.kind),
|
|
173
|
+
sinkId: safeAuditLabel(sinkId),
|
|
174
|
+
decision: 'deny',
|
|
175
|
+
reasonCodes: [],
|
|
176
|
+
redaction: 'not-needed',
|
|
177
|
+
redactionCount: 0,
|
|
178
|
+
redactionClasses: [],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function sanitizePayload(payload, options) {
|
|
182
|
+
if (typeof payload === 'string') {
|
|
183
|
+
const result = redactText(payload, options);
|
|
184
|
+
return { value: result.text, findings: result.findings };
|
|
185
|
+
}
|
|
186
|
+
const result = redactStructured(payload, options);
|
|
187
|
+
return { value: result.value, findings: result.findings };
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Mandatory sink boundary: minimize, redact, classify/gate, and attach lifecycle
|
|
191
|
+
* metadata before returning any publishable value. Failure returns no payload.
|
|
192
|
+
*/
|
|
193
|
+
export function prepareEvidenceForSink(input) {
|
|
194
|
+
const now = input.now ?? new Date();
|
|
195
|
+
const audit = emptyAudit(input.artifact, input.sinkId, now);
|
|
196
|
+
let policy;
|
|
197
|
+
try {
|
|
198
|
+
policy = resolveGovernancePolicy(input.policy);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
audit.reasonCodes = ['invalid-governance-policy'];
|
|
202
|
+
return { allowed: false, audit };
|
|
203
|
+
}
|
|
204
|
+
const sink = policy.sinks?.[input.sinkId];
|
|
205
|
+
if (!sink) {
|
|
206
|
+
audit.reasonCodes = ['unknown-sink'];
|
|
207
|
+
audit.redaction = 'failed';
|
|
208
|
+
return { allowed: false, audit };
|
|
209
|
+
}
|
|
210
|
+
let governance;
|
|
211
|
+
try {
|
|
212
|
+
governance = resolveArtifactGovernance({
|
|
213
|
+
kind: input.artifact.kind,
|
|
214
|
+
category: input.artifact.category,
|
|
215
|
+
metadata: input.artifact.governance,
|
|
216
|
+
parent: input.artifact.parentGovernance,
|
|
217
|
+
policy: policy.classification,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
catch {
|
|
221
|
+
audit.reasonCodes = ['invalid-classification-metadata'];
|
|
222
|
+
return { allowed: false, audit };
|
|
223
|
+
}
|
|
224
|
+
const tier = input.artifact.tier ?? 'durable';
|
|
225
|
+
let candidate;
|
|
226
|
+
try {
|
|
227
|
+
candidate = tier === 'raw' || input.artifact.category === 'sanitized-summary'
|
|
228
|
+
? input.artifact.payload
|
|
229
|
+
: minimizeEvidence(input.artifact.payload, input.maxExcerptBytes);
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
audit.reasonCodes = ['minimization-failed'];
|
|
233
|
+
return { allowed: false, audit };
|
|
234
|
+
}
|
|
235
|
+
let findings = [];
|
|
236
|
+
try {
|
|
237
|
+
const sanitized = sanitizePayload(candidate, policy.redaction ?? {});
|
|
238
|
+
candidate = sanitized.value;
|
|
239
|
+
findings = sanitized.findings;
|
|
240
|
+
audit.redaction = findings.length ? 'completed' : 'not-needed';
|
|
241
|
+
audit.redactionCount = findings.length;
|
|
242
|
+
audit.redactionClasses = [...new Set(findings.map((finding) => finding.class))].sort();
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
const override = input.redactionOverride;
|
|
246
|
+
if (!sink.allowRedactionOverride || !validOverride(override, input.artifact.id, sink.id, now.getTime())) {
|
|
247
|
+
audit.redaction = 'failed';
|
|
248
|
+
audit.reasonCodes = ['sanitization-failed'];
|
|
249
|
+
return { allowed: false, audit };
|
|
250
|
+
}
|
|
251
|
+
audit.redaction = 'override';
|
|
252
|
+
audit.decision = 'override';
|
|
253
|
+
audit.redactionOverrideId = override.id;
|
|
254
|
+
audit.redactionOverrideActor = override.actor;
|
|
255
|
+
audit.redactionOverrideReasonDigest = sha256(override.reason);
|
|
256
|
+
}
|
|
257
|
+
const gate = evaluatePublicationGate({
|
|
258
|
+
artifactId: input.artifact.id,
|
|
259
|
+
artifactKind: input.artifact.kind,
|
|
260
|
+
governance,
|
|
261
|
+
sink,
|
|
262
|
+
sourceRepository: input.sourceRepository,
|
|
263
|
+
approval: input.publicationApproval,
|
|
264
|
+
classes: resolveClassificationDefinitions(policy.classification),
|
|
265
|
+
now,
|
|
266
|
+
});
|
|
267
|
+
audit.publication = gate.audit;
|
|
268
|
+
audit.reasonCodes = gate.reasonCodes;
|
|
269
|
+
let summary = false;
|
|
270
|
+
const immutableSinkRequiresSummary = !sink.mutable && input.artifact.category !== 'sanitized-summary';
|
|
271
|
+
if (!gate.allowed || immutableSinkRequiresSummary) {
|
|
272
|
+
if (!gate.allowed && gate.decision !== 'summarize') {
|
|
273
|
+
audit.decision = 'deny';
|
|
274
|
+
return { allowed: false, audit };
|
|
275
|
+
}
|
|
276
|
+
candidate = createSanitizedSummary({
|
|
277
|
+
artifactId: input.artifact.id,
|
|
278
|
+
artifactKind: input.artifact.kind,
|
|
279
|
+
status: stableStatus(input.artifact.status),
|
|
280
|
+
omittedFields: candidate && typeof candidate === 'object' ? Object.keys(candidate).length : 1,
|
|
281
|
+
redactionClasses: audit.redactionClasses,
|
|
282
|
+
});
|
|
283
|
+
governance = resolveArtifactGovernance({
|
|
284
|
+
kind: 'SanitizedSummary',
|
|
285
|
+
category: 'sanitized-summary',
|
|
286
|
+
metadata: { classification: 'public', owner: governance.owner, handling: { allowedSinks: [sink.id], crossRepo: 'allow' } },
|
|
287
|
+
policy: policy.classification,
|
|
288
|
+
});
|
|
289
|
+
const summaryGate = evaluatePublicationGate({
|
|
290
|
+
artifactId: input.artifact.id,
|
|
291
|
+
artifactKind: 'SanitizedSummary',
|
|
292
|
+
governance,
|
|
293
|
+
sink,
|
|
294
|
+
sourceRepository: input.sourceRepository,
|
|
295
|
+
classes: resolveClassificationDefinitions(policy.classification),
|
|
296
|
+
now,
|
|
297
|
+
});
|
|
298
|
+
audit.publication = summaryGate.audit;
|
|
299
|
+
if (!summaryGate.allowed) {
|
|
300
|
+
audit.decision = 'deny';
|
|
301
|
+
audit.reasonCodes = ['sanitized-summary-denied', ...summaryGate.reasonCodes];
|
|
302
|
+
return { allowed: false, audit };
|
|
303
|
+
}
|
|
304
|
+
summary = true;
|
|
305
|
+
}
|
|
306
|
+
let lifecycle;
|
|
307
|
+
try {
|
|
308
|
+
lifecycle = createEvidenceLifecycle({
|
|
309
|
+
artifactId: input.artifact.id,
|
|
310
|
+
category: summary ? 'sanitized-summary' : input.artifact.category,
|
|
311
|
+
classification: governance.classification,
|
|
312
|
+
sink,
|
|
313
|
+
tier: summary ? 'durable' : tier,
|
|
314
|
+
rules: policy.retention,
|
|
315
|
+
requestedPolicyId: governance.handling.retentionPolicy,
|
|
316
|
+
rawCaptureReason: input.artifact.rawCaptureReason,
|
|
317
|
+
createdAt: now.toISOString(),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
audit.decision = 'deny';
|
|
322
|
+
audit.reasonCodes = ['retention-policy-unsatisfied'];
|
|
323
|
+
return { allowed: false, audit };
|
|
324
|
+
}
|
|
325
|
+
audit.retentionPolicyId = lifecycle.policyId;
|
|
326
|
+
audit.dispositionDeadline = lifecycle.dispositionDeadline;
|
|
327
|
+
audit.decision = summary ? 'summary' : gate.decision === 'override' || audit.redaction === 'override' ? 'override' : 'allow';
|
|
328
|
+
return {
|
|
329
|
+
allowed: true,
|
|
330
|
+
prepared: {
|
|
331
|
+
payload: candidate,
|
|
332
|
+
governance: {
|
|
333
|
+
classification: governance.classification,
|
|
334
|
+
...(governance.owner ? { owner: governance.owner } : {}),
|
|
335
|
+
handling: {
|
|
336
|
+
allowedSinks: governance.handling.allowedSinks ?? [sink.id],
|
|
337
|
+
crossRepo: governance.handling.crossRepo ?? 'approval-required',
|
|
338
|
+
retentionPolicy: lifecycle.policyId,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
lifecycle,
|
|
342
|
+
summary,
|
|
343
|
+
},
|
|
344
|
+
audit,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/** Call a sink writer only after the boundary returns publishable evidence. */
|
|
348
|
+
export async function publishEvidence(input) {
|
|
349
|
+
const result = prepareEvidenceForSink(input);
|
|
350
|
+
if (result.allowed && result.prepared)
|
|
351
|
+
await input.writer(result.prepared);
|
|
352
|
+
return result;
|
|
353
|
+
}
|
|
354
|
+
//# sourceMappingURL=boundary.js.map
|