@open-agent-toolkit/cli 0.2.65 → 0.2.66
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +19 -1
- package/assets/docs/cli-utilities/index.md +3 -0
- package/assets/docs/cli-utilities/remote-project-management.md +191 -0
- package/assets/docs/reference/cli-reference.md +12 -1
- package/assets/docs/reference/file-locations.md +22 -0
- package/assets/docs/reference/oat-directory-structure.md +30 -17
- package/assets/docs/reference/troubleshooting.md +44 -0
- package/assets/docs/workflows/skills/index.md +33 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-doctor/SKILL.md +4 -4
- package/assets/skills/oat-pjm-remote/SKILL.md +101 -0
- package/assets/skills/oat-pjm-remote/references/external-action-protocol.md +43 -0
- package/dist/commands/backlog/new.d.ts +1 -0
- package/dist/commands/backlog/new.d.ts.map +1 -1
- package/dist/commands/backlog/new.js +3 -1
- package/dist/commands/config/index.d.ts +1 -0
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +232 -1
- package/dist/commands/pjm/doctor.d.ts +2 -0
- package/dist/commands/pjm/doctor.d.ts.map +1 -1
- package/dist/commands/pjm/doctor.js +71 -1
- package/dist/commands/pjm/index.d.ts +2 -0
- package/dist/commands/pjm/index.d.ts.map +1 -1
- package/dist/commands/pjm/index.js +3 -0
- package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts +60 -0
- package/dist/commands/pjm/remote/__integration__/lifecycle-harness.d.ts.map +1 -0
- package/dist/commands/pjm/remote/__integration__/lifecycle-harness.js +151 -0
- package/dist/commands/pjm/remote/association.d.ts +27 -0
- package/dist/commands/pjm/remote/association.d.ts.map +1 -0
- package/dist/commands/pjm/remote/association.js +68 -0
- package/dist/commands/pjm/remote/authority.d.ts +210 -0
- package/dist/commands/pjm/remote/authority.d.ts.map +1 -0
- package/dist/commands/pjm/remote/authority.js +341 -0
- package/dist/commands/pjm/remote/batch.d.ts +15 -0
- package/dist/commands/pjm/remote/batch.d.ts.map +1 -0
- package/dist/commands/pjm/remote/batch.js +126 -0
- package/dist/commands/pjm/remote/closeout.d.ts +69 -0
- package/dist/commands/pjm/remote/closeout.d.ts.map +1 -0
- package/dist/commands/pjm/remote/closeout.js +208 -0
- package/dist/commands/pjm/remote/create-binding.d.ts +62 -0
- package/dist/commands/pjm/remote/create-binding.d.ts.map +1 -0
- package/dist/commands/pjm/remote/create-binding.js +126 -0
- package/dist/commands/pjm/remote/credential-safety.d.ts +7 -0
- package/dist/commands/pjm/remote/credential-safety.d.ts.map +1 -0
- package/dist/commands/pjm/remote/credential-safety.js +21 -0
- package/dist/commands/pjm/remote/discussion.d.ts +49 -0
- package/dist/commands/pjm/remote/discussion.d.ts.map +1 -0
- package/dist/commands/pjm/remote/discussion.js +124 -0
- package/dist/commands/pjm/remote/doctor.d.ts +22 -0
- package/dist/commands/pjm/remote/doctor.d.ts.map +1 -0
- package/dist/commands/pjm/remote/doctor.js +380 -0
- package/dist/commands/pjm/remote/external-action.d.ts +208 -0
- package/dist/commands/pjm/remote/external-action.d.ts.map +1 -0
- package/dist/commands/pjm/remote/external-action.js +367 -0
- package/dist/commands/pjm/remote/host-execution.d.ts +28 -0
- package/dist/commands/pjm/remote/host-execution.d.ts.map +1 -0
- package/dist/commands/pjm/remote/host-execution.js +63 -0
- package/dist/commands/pjm/remote/index.d.ts +41 -0
- package/dist/commands/pjm/remote/index.d.ts.map +1 -0
- package/dist/commands/pjm/remote/index.js +280 -0
- package/dist/commands/pjm/remote/jira-adf.d.ts +35 -0
- package/dist/commands/pjm/remote/jira-adf.d.ts.map +1 -0
- package/dist/commands/pjm/remote/jira-adf.js +169 -0
- package/dist/commands/pjm/remote/lifecycle.d.ts +163 -0
- package/dist/commands/pjm/remote/lifecycle.d.ts.map +1 -0
- package/dist/commands/pjm/remote/lifecycle.js +224 -0
- package/dist/commands/pjm/remote/local-projection.d.ts +30 -0
- package/dist/commands/pjm/remote/local-projection.d.ts.map +1 -0
- package/dist/commands/pjm/remote/local-projection.js +95 -0
- package/dist/commands/pjm/remote/managed-markdown.d.ts +30 -0
- package/dist/commands/pjm/remote/managed-markdown.d.ts.map +1 -0
- package/dist/commands/pjm/remote/managed-markdown.js +128 -0
- package/dist/commands/pjm/remote/migrate.d.ts +14 -0
- package/dist/commands/pjm/remote/migrate.d.ts.map +1 -0
- package/dist/commands/pjm/remote/migrate.js +32 -0
- package/dist/commands/pjm/remote/operation-state.d.ts +15 -0
- package/dist/commands/pjm/remote/operation-state.d.ts.map +1 -0
- package/dist/commands/pjm/remote/operation-state.js +143 -0
- package/dist/commands/pjm/remote/outbound-projection-safety.d.ts +25 -0
- package/dist/commands/pjm/remote/outbound-projection-safety.d.ts.map +1 -0
- package/dist/commands/pjm/remote/outbound-projection-safety.js +99 -0
- package/dist/commands/pjm/remote/output.d.ts +60 -0
- package/dist/commands/pjm/remote/output.d.ts.map +1 -0
- package/dist/commands/pjm/remote/output.js +48 -0
- package/dist/commands/pjm/remote/preview.d.ts +88 -0
- package/dist/commands/pjm/remote/preview.d.ts.map +1 -0
- package/dist/commands/pjm/remote/preview.js +154 -0
- package/dist/commands/pjm/remote/provider-conformance.d.ts +13 -0
- package/dist/commands/pjm/remote/provider-conformance.d.ts.map +1 -0
- package/dist/commands/pjm/remote/provider-conformance.js +37 -0
- package/dist/commands/pjm/remote/provider.d.ts +57 -0
- package/dist/commands/pjm/remote/provider.d.ts.map +1 -0
- package/dist/commands/pjm/remote/provider.js +28 -0
- package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts +46 -0
- package/dist/commands/pjm/remote/providers/github-publication-safety.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/github-publication-safety.js +126 -0
- package/dist/commands/pjm/remote/providers/github.d.ts +207 -0
- package/dist/commands/pjm/remote/providers/github.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/github.js +1456 -0
- package/dist/commands/pjm/remote/providers/jira.d.ts +245 -0
- package/dist/commands/pjm/remote/providers/jira.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/jira.js +1650 -0
- package/dist/commands/pjm/remote/providers/linear.d.ts +182 -0
- package/dist/commands/pjm/remote/providers/linear.d.ts.map +1 -0
- package/dist/commands/pjm/remote/providers/linear.js +1289 -0
- package/dist/commands/pjm/remote/purpose-policy.d.ts +29 -0
- package/dist/commands/pjm/remote/purpose-policy.d.ts.map +1 -0
- package/dist/commands/pjm/remote/purpose-policy.js +118 -0
- package/dist/commands/pjm/remote/reconcile.d.ts +42 -0
- package/dist/commands/pjm/remote/reconcile.d.ts.map +1 -0
- package/dist/commands/pjm/remote/reconcile.js +101 -0
- package/dist/commands/pjm/remote/resolution.d.ts +122 -0
- package/dist/commands/pjm/remote/resolution.d.ts.map +1 -0
- package/dist/commands/pjm/remote/resolution.js +243 -0
- package/dist/commands/pjm/remote/schema.d.ts +9309 -0
- package/dist/commands/pjm/remote/schema.d.ts.map +1 -0
- package/dist/commands/pjm/remote/schema.js +1503 -0
- package/dist/commands/pjm/remote/service.d.ts +20 -0
- package/dist/commands/pjm/remote/service.d.ts.map +1 -0
- package/dist/commands/pjm/remote/service.js +5121 -0
- package/dist/commands/pjm/remote/shared-storage.d.ts +36 -0
- package/dist/commands/pjm/remote/shared-storage.d.ts.map +1 -0
- package/dist/commands/pjm/remote/shared-storage.js +110 -0
- package/dist/commands/pjm/remote/snapshot.d.ts +32 -0
- package/dist/commands/pjm/remote/snapshot.d.ts.map +1 -0
- package/dist/commands/pjm/remote/snapshot.js +140 -0
- package/dist/commands/pjm/remote/storage-locator.d.ts +29 -0
- package/dist/commands/pjm/remote/storage-locator.d.ts.map +1 -0
- package/dist/commands/pjm/remote/storage-locator.js +62 -0
- package/dist/commands/pjm/remote/store.d.ts +85 -0
- package/dist/commands/pjm/remote/store.d.ts.map +1 -0
- package/dist/commands/pjm/remote/store.js +553 -0
- package/dist/commands/pjm/remote/verification.d.ts +40 -0
- package/dist/commands/pjm/remote/verification.d.ts.map +1 -0
- package/dist/commands/pjm/remote/verification.js +100 -0
- package/dist/commands/tools/shared/pack-manifest.d.ts.map +1 -1
- package/dist/commands/tools/shared/pack-manifest.js +1 -0
- package/dist/config/oat-config.d.ts +26 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +186 -2
- package/package.json +2 -2
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
export const OPERATION_STATES = [
|
|
2
|
+
'planned',
|
|
3
|
+
'pending',
|
|
4
|
+
'authorized',
|
|
5
|
+
'attempt-started',
|
|
6
|
+
'verification-pending',
|
|
7
|
+
'blocked',
|
|
8
|
+
'verified',
|
|
9
|
+
'partial',
|
|
10
|
+
'uncertain',
|
|
11
|
+
'failed',
|
|
12
|
+
'rejected',
|
|
13
|
+
];
|
|
14
|
+
const VALID_TRANSITIONS = {
|
|
15
|
+
planned: new Set(['pending', 'authorized', 'blocked', 'failed']),
|
|
16
|
+
pending: new Set(['authorized', 'blocked', 'failed']),
|
|
17
|
+
authorized: new Set(['attempt-started', 'blocked', 'failed']),
|
|
18
|
+
'attempt-started': new Set([
|
|
19
|
+
'verification-pending',
|
|
20
|
+
'partial',
|
|
21
|
+
'uncertain',
|
|
22
|
+
'rejected',
|
|
23
|
+
]),
|
|
24
|
+
'verification-pending': new Set(['verified', 'partial', 'uncertain']),
|
|
25
|
+
blocked: new Set(),
|
|
26
|
+
verified: new Set(),
|
|
27
|
+
partial: new Set(),
|
|
28
|
+
uncertain: new Set(),
|
|
29
|
+
failed: new Set(),
|
|
30
|
+
rejected: new Set(),
|
|
31
|
+
};
|
|
32
|
+
const ATTEMPTED_STATES = new Set([
|
|
33
|
+
'attempt-started',
|
|
34
|
+
'verification-pending',
|
|
35
|
+
'verified',
|
|
36
|
+
'partial',
|
|
37
|
+
'uncertain',
|
|
38
|
+
'rejected',
|
|
39
|
+
]);
|
|
40
|
+
const ADVERSE_STATES = new Set([
|
|
41
|
+
'partial',
|
|
42
|
+
'uncertain',
|
|
43
|
+
'rejected',
|
|
44
|
+
'blocked',
|
|
45
|
+
'failed',
|
|
46
|
+
]);
|
|
47
|
+
export function transitionRemoteOperation(current, next, subject = 'parent') {
|
|
48
|
+
if (!VALID_TRANSITIONS[current].has(next)) {
|
|
49
|
+
throw new Error(`Invalid remote ${subject} transition from '${current}' to '${next}'.`);
|
|
50
|
+
}
|
|
51
|
+
return next;
|
|
52
|
+
}
|
|
53
|
+
export function reduceCompositeOperationState(substeps) {
|
|
54
|
+
if (substeps.length === 0) {
|
|
55
|
+
throw new Error('Composite operation reduction requires substeps.');
|
|
56
|
+
}
|
|
57
|
+
const ids = substeps.map((substep) => substep.substepId);
|
|
58
|
+
if (new Set(ids).size !== ids.length) {
|
|
59
|
+
throw new Error('Composite substep IDs must be unique.');
|
|
60
|
+
}
|
|
61
|
+
for (const [index, substep] of substeps.entries()) {
|
|
62
|
+
if (index > 0 &&
|
|
63
|
+
ATTEMPTED_STATES.has(substep.state) &&
|
|
64
|
+
substeps
|
|
65
|
+
.slice(0, index)
|
|
66
|
+
.some((dependency) => dependency.state !== 'verified')) {
|
|
67
|
+
throw new Error(`Composite substep '${substep.substepId}' attempted before its dependency verified.`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const verifiedSubstepIds = substeps
|
|
71
|
+
.filter((substep) => substep.state === 'verified')
|
|
72
|
+
.map((substep) => substep.substepId);
|
|
73
|
+
const allVerified = verifiedSubstepIds.length === substeps.length;
|
|
74
|
+
if (allVerified) {
|
|
75
|
+
return {
|
|
76
|
+
state: 'verified',
|
|
77
|
+
verifiedSubstepIds,
|
|
78
|
+
continuableSubstepId: null,
|
|
79
|
+
retryDisposition: 'not-applicable',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const states = new Set(substeps.map((substep) => substep.state));
|
|
83
|
+
let state;
|
|
84
|
+
if (states.has('partial') ||
|
|
85
|
+
(verifiedSubstepIds.length > 0 &&
|
|
86
|
+
substeps.some((substep) => ADVERSE_STATES.has(substep.state)))) {
|
|
87
|
+
state = 'partial';
|
|
88
|
+
}
|
|
89
|
+
else if (states.has('uncertain')) {
|
|
90
|
+
state = 'uncertain';
|
|
91
|
+
}
|
|
92
|
+
else if (states.has('rejected')) {
|
|
93
|
+
state = 'rejected';
|
|
94
|
+
}
|
|
95
|
+
else if (states.has('verification-pending')) {
|
|
96
|
+
state = 'verification-pending';
|
|
97
|
+
}
|
|
98
|
+
else if (states.has('attempt-started')) {
|
|
99
|
+
state = 'attempt-started';
|
|
100
|
+
}
|
|
101
|
+
else if (states.has('failed')) {
|
|
102
|
+
state = 'failed';
|
|
103
|
+
}
|
|
104
|
+
else if (states.has('blocked')) {
|
|
105
|
+
state = 'blocked';
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const next = firstContinuableSubstep(substeps);
|
|
109
|
+
state = next?.state ?? 'planned';
|
|
110
|
+
}
|
|
111
|
+
const reconcileRequired = state === 'partial' ||
|
|
112
|
+
state === 'uncertain' ||
|
|
113
|
+
state === 'rejected' ||
|
|
114
|
+
state === 'attempt-started' ||
|
|
115
|
+
state === 'verification-pending';
|
|
116
|
+
const continuable = reconcileRequired
|
|
117
|
+
? null
|
|
118
|
+
: (firstContinuableSubstep(substeps)?.substepId ?? null);
|
|
119
|
+
return {
|
|
120
|
+
state,
|
|
121
|
+
verifiedSubstepIds,
|
|
122
|
+
continuableSubstepId: continuable,
|
|
123
|
+
retryDisposition: reconcileRequired
|
|
124
|
+
? 'reconcile-required'
|
|
125
|
+
: continuable
|
|
126
|
+
? 'safe-before-attempt'
|
|
127
|
+
: 'not-applicable',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function firstContinuableSubstep(substeps) {
|
|
131
|
+
for (const [index, substep] of substeps.entries()) {
|
|
132
|
+
if (substep.state === 'verified')
|
|
133
|
+
continue;
|
|
134
|
+
if (!['planned', 'pending', 'authorized'].includes(substep.state) ||
|
|
135
|
+
substeps
|
|
136
|
+
.slice(0, index)
|
|
137
|
+
.some((dependency) => dependency.state !== 'verified')) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
return substep;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface OutboundProjection {
|
|
2
|
+
title?: string;
|
|
3
|
+
description?: string | null;
|
|
4
|
+
priority?: string | null;
|
|
5
|
+
status?: string;
|
|
6
|
+
annotation?: string;
|
|
7
|
+
sourceRevision?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface OutboundProjectionSafetyResult {
|
|
10
|
+
schemaVersion: 1;
|
|
11
|
+
projectionDigest: string;
|
|
12
|
+
verdict: 'safe' | 'blocked';
|
|
13
|
+
resultDigest: string;
|
|
14
|
+
reasons: Array<{
|
|
15
|
+
code: 'sensitive-content' | 'private-artifact' | 'invalid-projection';
|
|
16
|
+
field: keyof OutboundProjection | null;
|
|
17
|
+
}>;
|
|
18
|
+
assessedAt: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function digestOutboundProjection(projection: OutboundProjection): string;
|
|
21
|
+
export declare function assessOutboundProjectionSafety(projection: OutboundProjection, options: {
|
|
22
|
+
assessedAt: string;
|
|
23
|
+
}): OutboundProjectionSafetyResult;
|
|
24
|
+
export declare function requireCurrentOutboundSafety(projection: OutboundProjection, result: OutboundProjectionSafetyResult | null | undefined): asserts result is OutboundProjectionSafetyResult;
|
|
25
|
+
//# sourceMappingURL=outbound-projection-safety.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"outbound-projection-safety.d.ts","sourceRoot":"","sources":["../../../../src/commands/pjm/remote/outbound-projection-safety.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,kBAAkB;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,8BAA8B;IAC7C,aAAa,EAAE,CAAC,CAAC;IACjB,gBAAgB,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,KAAK,CAAC;QACb,IAAI,EAAE,mBAAmB,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;QACtE,KAAK,EAAE,MAAM,kBAAkB,GAAG,IAAI,CAAC;KACxC,CAAC,CAAC;IACH,UAAU,EAAE,MAAM,CAAC;CACpB;AAiBD,wBAAgB,wBAAwB,CACtC,UAAU,EAAE,kBAAkB,GAC7B,MAAM,CAER;AAED,wBAAgB,8BAA8B,CAC5C,UAAU,EAAE,kBAAkB,EAC9B,OAAO,EAAE;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAC9B,8BAA8B,CA8ChC;AAED,wBAAgB,4BAA4B,CAC1C,UAAU,EAAE,kBAAkB,EAC9B,MAAM,EAAE,8BAA8B,GAAG,IAAI,GAAG,SAAS,GACxD,OAAO,CAAC,MAAM,IAAI,8BAA8B,CAiBlD"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { containsSensitiveContentSignalInValue } from './credential-safety.js';
|
|
3
|
+
const ALLOWED_FIELDS = new Set([
|
|
4
|
+
'title',
|
|
5
|
+
'description',
|
|
6
|
+
'priority',
|
|
7
|
+
'status',
|
|
8
|
+
'annotation',
|
|
9
|
+
'sourceRevision',
|
|
10
|
+
]);
|
|
11
|
+
const PRIVATE_ARTIFACT_MARKERS = [
|
|
12
|
+
'.oat/projects/',
|
|
13
|
+
'discovery.md',
|
|
14
|
+
'implementation.md',
|
|
15
|
+
'review artifact',
|
|
16
|
+
];
|
|
17
|
+
export function digestOutboundProjection(projection) {
|
|
18
|
+
return digest(projection);
|
|
19
|
+
}
|
|
20
|
+
export function assessOutboundProjectionSafety(projection, options) {
|
|
21
|
+
if (!Number.isFinite(Date.parse(options.assessedAt))) {
|
|
22
|
+
throw new Error('Outbound safety assessment requires a valid timestamp.');
|
|
23
|
+
}
|
|
24
|
+
const reasons = [];
|
|
25
|
+
for (const [rawField, value] of Object.entries(projection)) {
|
|
26
|
+
const field = rawField;
|
|
27
|
+
if (!ALLOWED_FIELDS.has(field) ||
|
|
28
|
+
(value !== null && typeof value !== 'string')) {
|
|
29
|
+
reasons.push({
|
|
30
|
+
code: 'invalid-projection',
|
|
31
|
+
field: ALLOWED_FIELDS.has(field) ? field : null,
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (containsSensitiveContentSignalInValue(value)) {
|
|
36
|
+
reasons.push({ code: 'sensitive-content', field });
|
|
37
|
+
}
|
|
38
|
+
if (typeof value === 'string' &&
|
|
39
|
+
PRIVATE_ARTIFACT_MARKERS.some((marker) => value.toLowerCase().includes(marker))) {
|
|
40
|
+
reasons.push({ code: 'private-artifact', field });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const projectionDigest = digest(projection);
|
|
44
|
+
const verdict = reasons.length === 0 ? 'safe' : 'blocked';
|
|
45
|
+
const resultDigest = digest({
|
|
46
|
+
schemaVersion: 1,
|
|
47
|
+
projectionDigest,
|
|
48
|
+
verdict,
|
|
49
|
+
reasons,
|
|
50
|
+
assessedAt: options.assessedAt,
|
|
51
|
+
});
|
|
52
|
+
return {
|
|
53
|
+
schemaVersion: 1,
|
|
54
|
+
projectionDigest,
|
|
55
|
+
verdict,
|
|
56
|
+
resultDigest,
|
|
57
|
+
reasons,
|
|
58
|
+
assessedAt: options.assessedAt,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function requireCurrentOutboundSafety(projection, result) {
|
|
62
|
+
if (!result)
|
|
63
|
+
throw new Error('Outbound safety evidence is missing.');
|
|
64
|
+
if (result.verdict !== 'safe')
|
|
65
|
+
throw new Error('Outbound safety evidence blocks execution.');
|
|
66
|
+
if (result.projectionDigest !== digestOutboundProjection(projection)) {
|
|
67
|
+
throw new Error('Outbound safety evidence is stale or mismatched.');
|
|
68
|
+
}
|
|
69
|
+
const expectedResultDigest = digest({
|
|
70
|
+
schemaVersion: result.schemaVersion,
|
|
71
|
+
projectionDigest: result.projectionDigest,
|
|
72
|
+
verdict: result.verdict,
|
|
73
|
+
reasons: result.reasons,
|
|
74
|
+
assessedAt: result.assessedAt,
|
|
75
|
+
});
|
|
76
|
+
if (expectedResultDigest !== result.resultDigest) {
|
|
77
|
+
throw new Error('Outbound safety result digest is invalid.');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function digest(value) {
|
|
81
|
+
return `sha256:${createHash('sha256')
|
|
82
|
+
.update(JSON.stringify(canonicalize(value)))
|
|
83
|
+
.digest('hex')}`;
|
|
84
|
+
}
|
|
85
|
+
function canonicalize(value) {
|
|
86
|
+
if (value === null || ['string', 'boolean'].includes(typeof value))
|
|
87
|
+
return value;
|
|
88
|
+
if (typeof value === 'number')
|
|
89
|
+
return value;
|
|
90
|
+
if (Array.isArray(value))
|
|
91
|
+
return value.map(canonicalize);
|
|
92
|
+
if (value && typeof value === 'object') {
|
|
93
|
+
return Object.fromEntries(Object.entries(value)
|
|
94
|
+
.filter(([, item]) => item !== undefined)
|
|
95
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
96
|
+
.map(([key, item]) => [key, canonicalize(item)]));
|
|
97
|
+
}
|
|
98
|
+
throw new Error('Outbound projection contains an unsupported value.');
|
|
99
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { DiscussionEvidence } from './discussion.js';
|
|
2
|
+
import type { ExternalActionEnvelope } from './external-action.js';
|
|
3
|
+
import type { BindingPreview } from './preview.js';
|
|
4
|
+
export type RemoteCommandStatus = 'ok' | 'pending' | 'needs-review' | 'partial' | 'uncertain' | 'blocked' | 'rejected' | 'failed';
|
|
5
|
+
export interface RemoteCommandEnvelope {
|
|
6
|
+
schemaVersion: 1;
|
|
7
|
+
status: RemoteCommandStatus;
|
|
8
|
+
operation: string;
|
|
9
|
+
projectRoot: string;
|
|
10
|
+
persisted: boolean;
|
|
11
|
+
results: Array<{
|
|
12
|
+
bindingId: string;
|
|
13
|
+
provider: 'github' | 'linear' | 'jira';
|
|
14
|
+
target: string;
|
|
15
|
+
status: RemoteCommandStatus;
|
|
16
|
+
freshness: string;
|
|
17
|
+
authority: string;
|
|
18
|
+
diagnosticCode: string | null;
|
|
19
|
+
}>;
|
|
20
|
+
externalAction: ExternalActionEnvelope | null;
|
|
21
|
+
discussionEvidence?: DiscussionEvidence;
|
|
22
|
+
approvalPreview?: {
|
|
23
|
+
operationId: string;
|
|
24
|
+
digest: string;
|
|
25
|
+
operationClass: BindingPreview['operationClass'];
|
|
26
|
+
fieldMask: BindingPreview['fieldMask'];
|
|
27
|
+
renderedFields: Partial<BindingPreview['renderedFields']>;
|
|
28
|
+
authority: string;
|
|
29
|
+
componentDigests: {
|
|
30
|
+
target: string;
|
|
31
|
+
baseline: string;
|
|
32
|
+
capability: string;
|
|
33
|
+
authority: string;
|
|
34
|
+
policy: string;
|
|
35
|
+
projection: string;
|
|
36
|
+
outboundSafety: string;
|
|
37
|
+
};
|
|
38
|
+
revision: {
|
|
39
|
+
digest: string;
|
|
40
|
+
evidenceDigest: string;
|
|
41
|
+
source: 'remote' | 'remote-unobserved' | 'local-source-unbound';
|
|
42
|
+
strength: 'token' | 'updated-at-and-hash' | 'hash-only' | 'unknown';
|
|
43
|
+
updatedAt: string | null;
|
|
44
|
+
observedAt: string | null;
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
recovery: Array<{
|
|
48
|
+
code: string;
|
|
49
|
+
instruction: string;
|
|
50
|
+
}>;
|
|
51
|
+
}
|
|
52
|
+
export interface RenderedRemoteCommand {
|
|
53
|
+
stdout: string;
|
|
54
|
+
stderr: string;
|
|
55
|
+
exitCode: 0 | 1 | 2;
|
|
56
|
+
}
|
|
57
|
+
export declare function renderRemoteCommand(envelope: RemoteCommandEnvelope, options: {
|
|
58
|
+
json: boolean;
|
|
59
|
+
}): RenderedRemoteCommand;
|
|
60
|
+
//# sourceMappingURL=output.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../../../src/commands/pjm/remote/output.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD,MAAM,MAAM,mBAAmB,GAC3B,IAAI,GACJ,SAAS,GACT,cAAc,GACd,SAAS,GACT,WAAW,GACX,SAAS,GACT,UAAU,GACV,QAAQ,CAAC;AAEb,MAAM,WAAW,qBAAqB;IACpC,aAAa,EAAE,CAAC,CAAC;IACjB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,KAAK,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,QAAQ,EAAE,QAAQ,GAAG,QAAQ,GAAG,MAAM,CAAC;QACvC,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,mBAAmB,CAAC;QAC5B,SAAS,EAAE,MAAM,CAAC;QAClB,SAAS,EAAE,MAAM,CAAC;QAClB,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;KAC/B,CAAC,CAAC;IACH,cAAc,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAC9C,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,eAAe,CAAC,EAAE;QAChB,WAAW,EAAE,MAAM,CAAC;QACpB,MAAM,EAAE,MAAM,CAAC;QACf,cAAc,EAAE,cAAc,CAAC,gBAAgB,CAAC,CAAC;QACjD,SAAS,EAAE,cAAc,CAAC,WAAW,CAAC,CAAC;QACvC,cAAc,EAAE,OAAO,CAAC,cAAc,CAAC,gBAAgB,CAAC,CAAC,CAAC;QAC1D,SAAS,EAAE,MAAM,CAAC;QAClB,gBAAgB,EAAE;YAChB,MAAM,EAAE,MAAM,CAAC;YACf,QAAQ,EAAE,MAAM,CAAC;YACjB,UAAU,EAAE,MAAM,CAAC;YACnB,SAAS,EAAE,MAAM,CAAC;YAClB,MAAM,EAAE,MAAM,CAAC;YACf,UAAU,EAAE,MAAM,CAAC;YACnB,cAAc,EAAE,MAAM,CAAC;SACxB,CAAC;QACF,QAAQ,EAAE;YACR,MAAM,EAAE,MAAM,CAAC;YACf,cAAc,EAAE,MAAM,CAAC;YACvB,MAAM,EAAE,QAAQ,GAAG,mBAAmB,GAAG,sBAAsB,CAAC;YAChE,QAAQ,EAAE,OAAO,GAAG,qBAAqB,GAAG,WAAW,GAAG,SAAS,CAAC;YACpE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;YACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;SAC3B,CAAC;KACH,CAAC;IACF,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACxD;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;CACrB;AAED,wBAAgB,mBAAmB,CACjC,QAAQ,EAAE,qBAAqB,EAC/B,OAAO,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,GACzB,qBAAqB,CAyDvB"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export function renderRemoteCommand(envelope, options) {
|
|
2
|
+
validateEnvelope(envelope);
|
|
3
|
+
const exitCode = envelope.status === 'ok' ? 0 : envelope.status === 'failed' ? 2 : 1;
|
|
4
|
+
if (options.json) {
|
|
5
|
+
return { stdout: `${JSON.stringify(envelope)}\n`, stderr: '', exitCode };
|
|
6
|
+
}
|
|
7
|
+
const lines = [
|
|
8
|
+
`${envelope.operation}: ${envelope.status}`,
|
|
9
|
+
`persisted: ${envelope.persisted ? 'yes' : 'no'}`,
|
|
10
|
+
...envelope.results.map((result) => `${result.provider} ${result.target} [${result.bindingId}]: ${result.status}; freshness=${result.freshness}; authority=${result.authority}${result.diagnosticCode ? `; code=${result.diagnosticCode}` : ''}`),
|
|
11
|
+
...envelope.recovery.map((item) => `recovery ${item.code}: ${item.instruction}`),
|
|
12
|
+
];
|
|
13
|
+
if (envelope.externalAction)
|
|
14
|
+
lines.push('external action: durable handoff required');
|
|
15
|
+
if (envelope.discussionEvidence) {
|
|
16
|
+
lines.push(`discussion evidence: ${envelope.discussionEvidence.status}; items=${envelope.discussionEvidence.items.length}; pages=${envelope.discussionEvidence.pagesRead}; truncated=${String(envelope.discussionEvidence.truncated)}; persisted=${String(envelope.discussionEvidence.persisted)}`);
|
|
17
|
+
lines.push(...envelope.discussionEvidence.items.map((item) => `discussion item ${item.id}: ${item.body}${item.incomplete ? ' (incomplete)' : ''}`));
|
|
18
|
+
}
|
|
19
|
+
if (envelope.approvalPreview) {
|
|
20
|
+
const preview = envelope.approvalPreview;
|
|
21
|
+
lines.push(`preview ${preview.operationId}: ${preview.operationClass}; digest=${preview.digest}; fields=${preview.fieldMask.join(',')}; authority=${preview.authority}; revision=${preview.revision.digest}; revision-evidence=${preview.revision.evidenceDigest}; revision-source=${preview.revision.source}; strength=${preview.revision.strength}; updated=${preview.revision.updatedAt ?? 'unobserved'}; observed=${preview.revision.observedAt ?? 'unobserved'}`);
|
|
22
|
+
lines.push(`components target=${preview.componentDigests.target}; baseline=${preview.componentDigests.baseline}; capability=${preview.componentDigests.capability}; authority=${preview.componentDigests.authority}; policy=${preview.componentDigests.policy}; projection=${preview.componentDigests.projection}; safety=${preview.componentDigests.outboundSafety}`);
|
|
23
|
+
for (const field of preview.fieldMask) {
|
|
24
|
+
const rendered = preview.renderedFields[field];
|
|
25
|
+
if (!rendered) {
|
|
26
|
+
throw new Error(`Approval preview is missing rendered field '${field}'.`);
|
|
27
|
+
}
|
|
28
|
+
lines.push(rendered.kind === 'hash'
|
|
29
|
+
? `preview field ${field}: hash=${rendered.digest}; bytes=${rendered.bytes}`
|
|
30
|
+
: `preview field ${field}: value=${JSON.stringify(rendered.value)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const rendered = `${lines.join('\n')}\n`;
|
|
34
|
+
return envelope.status === 'failed'
|
|
35
|
+
? { stdout: '', stderr: rendered, exitCode }
|
|
36
|
+
: { stdout: rendered, stderr: '', exitCode };
|
|
37
|
+
}
|
|
38
|
+
function validateEnvelope(envelope) {
|
|
39
|
+
if (envelope.schemaVersion !== 1)
|
|
40
|
+
throw new Error('Unsupported remote command envelope version.');
|
|
41
|
+
if (envelope.status === 'ok' && !envelope.persisted) {
|
|
42
|
+
throw new Error('A verified remote outcome must be durably persisted.');
|
|
43
|
+
}
|
|
44
|
+
if (envelope.externalAction &&
|
|
45
|
+
(!envelope.persisted || envelope.status !== 'pending')) {
|
|
46
|
+
throw new Error('An external action requires a persisted pending handoff.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { OatPjmRemoteOperationClass, OatPjmRemoteProvider } from '../../../config/oat-config.js';
|
|
2
|
+
import { type OutboundProjectionSafetyResult } from './outbound-projection-safety.js';
|
|
3
|
+
import type { SharedRemoteField } from './purpose-policy.js';
|
|
4
|
+
export interface BuildBindingPreviewInput {
|
|
5
|
+
binding: {
|
|
6
|
+
bindingId: string;
|
|
7
|
+
provider: OatPjmRemoteProvider;
|
|
8
|
+
purposes: string[];
|
|
9
|
+
};
|
|
10
|
+
target: {
|
|
11
|
+
stableId: string;
|
|
12
|
+
context: Record<string, string>;
|
|
13
|
+
};
|
|
14
|
+
baseline: {
|
|
15
|
+
baselineId: string;
|
|
16
|
+
digest: string;
|
|
17
|
+
} | null;
|
|
18
|
+
revision: {
|
|
19
|
+
strength: string;
|
|
20
|
+
token: string | null;
|
|
21
|
+
updatedAt: string | null;
|
|
22
|
+
contentHash: string;
|
|
23
|
+
};
|
|
24
|
+
capability: {
|
|
25
|
+
surfaceKind: 'connector' | 'configured-cli';
|
|
26
|
+
evidenceDigest: string;
|
|
27
|
+
semanticCapabilities: string[];
|
|
28
|
+
context: Record<string, string>;
|
|
29
|
+
};
|
|
30
|
+
policy: Record<string, unknown>;
|
|
31
|
+
projection: {
|
|
32
|
+
title: string;
|
|
33
|
+
description: string | null;
|
|
34
|
+
priority: string | null;
|
|
35
|
+
};
|
|
36
|
+
outboundSafety: OutboundProjectionSafetyResult;
|
|
37
|
+
operationClass: OatPjmRemoteOperationClass;
|
|
38
|
+
fieldMask: SharedRemoteField[];
|
|
39
|
+
createdAt: string;
|
|
40
|
+
}
|
|
41
|
+
type RenderedField = {
|
|
42
|
+
kind: 'value';
|
|
43
|
+
value: string | null;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'hash';
|
|
46
|
+
digest: string;
|
|
47
|
+
bytes: number;
|
|
48
|
+
};
|
|
49
|
+
export interface BindingPreview {
|
|
50
|
+
schemaVersion: 1;
|
|
51
|
+
digest: string;
|
|
52
|
+
bindingId: string;
|
|
53
|
+
provider: OatPjmRemoteProvider;
|
|
54
|
+
operationClass: OatPjmRemoteOperationClass;
|
|
55
|
+
fieldMask: SharedRemoteField[];
|
|
56
|
+
createdAt: string;
|
|
57
|
+
componentDigests: {
|
|
58
|
+
target: string;
|
|
59
|
+
baseline: string;
|
|
60
|
+
revision: string;
|
|
61
|
+
capability: string;
|
|
62
|
+
policy: string;
|
|
63
|
+
projection: string;
|
|
64
|
+
outboundSafety: string;
|
|
65
|
+
};
|
|
66
|
+
renderedFields: Record<SharedRemoteField, RenderedField>;
|
|
67
|
+
}
|
|
68
|
+
export interface PreviewApproval {
|
|
69
|
+
previewDigest: string;
|
|
70
|
+
operationClass: OatPjmRemoteOperationClass;
|
|
71
|
+
approvedAt: string;
|
|
72
|
+
actor: string;
|
|
73
|
+
source: string;
|
|
74
|
+
}
|
|
75
|
+
export type ApprovalValidationReason = 'digest-mismatch' | 'operation-mismatch' | 'invalid-time' | 'approval-before-preview' | 'future-approval' | 'expired' | 'unsafe-evidence';
|
|
76
|
+
export declare function buildBindingPreview(input: BuildBindingPreviewInput): BindingPreview;
|
|
77
|
+
export declare function validatePreviewApproval(preview: BindingPreview, approval: PreviewApproval, options: {
|
|
78
|
+
now: string;
|
|
79
|
+
maxAgeMs: number;
|
|
80
|
+
}): {
|
|
81
|
+
valid: true;
|
|
82
|
+
reason: null;
|
|
83
|
+
} | {
|
|
84
|
+
valid: false;
|
|
85
|
+
reason: ApprovalValidationReason;
|
|
86
|
+
};
|
|
87
|
+
export {};
|
|
88
|
+
//# sourceMappingURL=preview.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../../../../src/commands/pjm/remote/preview.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { containsSensitiveContentSignal } from './credential-safety.js';
|
|
3
|
+
import { requireCurrentOutboundSafety, } from './outbound-projection-safety.js';
|
|
4
|
+
import { WHOLE_FIELD_SUPPRESSION_MARKER } from './schema.js';
|
|
5
|
+
const FIELD_ORDER = [
|
|
6
|
+
'title',
|
|
7
|
+
'description',
|
|
8
|
+
'priority',
|
|
9
|
+
];
|
|
10
|
+
export function buildBindingPreview(input) {
|
|
11
|
+
assertTimestamp(input.createdAt, 'preview creation');
|
|
12
|
+
if (canonicalJson(input.target.context) !==
|
|
13
|
+
canonicalJson(input.capability.context)) {
|
|
14
|
+
throw new Error('Preview target and capability contexts must match.');
|
|
15
|
+
}
|
|
16
|
+
const fieldMask = normalizeFieldMask(input.fieldMask);
|
|
17
|
+
requireCurrentOutboundSafety(input.projection, input.outboundSafety);
|
|
18
|
+
const componentDigests = {
|
|
19
|
+
target: hashCanonical(input.target),
|
|
20
|
+
baseline: hashCanonical(input.baseline),
|
|
21
|
+
revision: hashCanonical(input.revision),
|
|
22
|
+
capability: hashCanonical(input.capability),
|
|
23
|
+
policy: hashCanonical(input.policy),
|
|
24
|
+
projection: input.outboundSafety.projectionDigest,
|
|
25
|
+
outboundSafety: input.outboundSafety.resultDigest,
|
|
26
|
+
};
|
|
27
|
+
const digest = hashCanonical({
|
|
28
|
+
schemaVersion: 1,
|
|
29
|
+
binding: input.binding,
|
|
30
|
+
operationClass: input.operationClass,
|
|
31
|
+
fieldMask,
|
|
32
|
+
createdAt: input.createdAt,
|
|
33
|
+
componentDigests,
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
schemaVersion: 1,
|
|
37
|
+
digest,
|
|
38
|
+
bindingId: input.binding.bindingId,
|
|
39
|
+
provider: input.binding.provider,
|
|
40
|
+
operationClass: input.operationClass,
|
|
41
|
+
fieldMask,
|
|
42
|
+
createdAt: input.createdAt,
|
|
43
|
+
componentDigests,
|
|
44
|
+
renderedFields: {
|
|
45
|
+
title: renderConcise(input.projection.title),
|
|
46
|
+
description: renderBody(input.projection.description),
|
|
47
|
+
priority: renderConcise(input.projection.priority),
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function validatePreviewApproval(preview, approval, options) {
|
|
52
|
+
if (approval.previewDigest !== preview.digest) {
|
|
53
|
+
return { valid: false, reason: 'digest-mismatch' };
|
|
54
|
+
}
|
|
55
|
+
if (approval.operationClass !== preview.operationClass) {
|
|
56
|
+
return { valid: false, reason: 'operation-mismatch' };
|
|
57
|
+
}
|
|
58
|
+
const approvedAt = Date.parse(approval.approvedAt);
|
|
59
|
+
const previewCreatedAt = Date.parse(preview.createdAt);
|
|
60
|
+
const now = Date.parse(options.now);
|
|
61
|
+
if (!Number.isFinite(approvedAt) ||
|
|
62
|
+
!Number.isFinite(previewCreatedAt) ||
|
|
63
|
+
!Number.isFinite(now) ||
|
|
64
|
+
!Number.isFinite(options.maxAgeMs) ||
|
|
65
|
+
options.maxAgeMs < 0) {
|
|
66
|
+
return { valid: false, reason: 'invalid-time' };
|
|
67
|
+
}
|
|
68
|
+
if (approvedAt < previewCreatedAt) {
|
|
69
|
+
return { valid: false, reason: 'approval-before-preview' };
|
|
70
|
+
}
|
|
71
|
+
if (approvedAt > now)
|
|
72
|
+
return { valid: false, reason: 'future-approval' };
|
|
73
|
+
if (now - approvedAt > options.maxAgeMs) {
|
|
74
|
+
return { valid: false, reason: 'expired' };
|
|
75
|
+
}
|
|
76
|
+
if (!isSafeEvidence(approval.actor) || !isSafeEvidence(approval.source)) {
|
|
77
|
+
return { valid: false, reason: 'unsafe-evidence' };
|
|
78
|
+
}
|
|
79
|
+
return { valid: true, reason: null };
|
|
80
|
+
}
|
|
81
|
+
function normalizeFieldMask(fieldMask) {
|
|
82
|
+
const unique = new Set(fieldMask);
|
|
83
|
+
if (unique.size !== fieldMask.length || fieldMask.length === 0) {
|
|
84
|
+
throw new Error('Preview field mask must be non-empty and unique.');
|
|
85
|
+
}
|
|
86
|
+
for (const field of unique) {
|
|
87
|
+
if (!FIELD_ORDER.includes(field)) {
|
|
88
|
+
throw new Error(`Unsupported preview field '${String(field)}'.`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return FIELD_ORDER.filter((field) => unique.has(field));
|
|
92
|
+
}
|
|
93
|
+
function renderConcise(value) {
|
|
94
|
+
if (value === null)
|
|
95
|
+
return { kind: 'value', value: null };
|
|
96
|
+
if (containsSensitiveContentSignal(value)) {
|
|
97
|
+
return { kind: 'value', value: WHOLE_FIELD_SUPPRESSION_MARKER };
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
kind: 'value',
|
|
101
|
+
value: value.length > 160 ? `${value.slice(0, 157)}...` : value,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function renderBody(value) {
|
|
105
|
+
if (value === null)
|
|
106
|
+
return { kind: 'value', value: null };
|
|
107
|
+
return {
|
|
108
|
+
kind: 'hash',
|
|
109
|
+
digest: hashText(value),
|
|
110
|
+
bytes: Buffer.byteLength(value, 'utf8'),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function isSafeEvidence(value) {
|
|
114
|
+
return (value.length > 0 &&
|
|
115
|
+
value.length <= 255 &&
|
|
116
|
+
!/[\r\n]/.test(value) &&
|
|
117
|
+
!value.includes('\0') &&
|
|
118
|
+
!containsSensitiveContentSignal(value));
|
|
119
|
+
}
|
|
120
|
+
function hashCanonical(value) {
|
|
121
|
+
return hashText(canonicalJson(value));
|
|
122
|
+
}
|
|
123
|
+
function hashText(value) {
|
|
124
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
125
|
+
}
|
|
126
|
+
function canonicalJson(value) {
|
|
127
|
+
return JSON.stringify(canonicalize(value));
|
|
128
|
+
}
|
|
129
|
+
function canonicalize(value) {
|
|
130
|
+
if (value === null ||
|
|
131
|
+
typeof value === 'string' ||
|
|
132
|
+
typeof value === 'boolean') {
|
|
133
|
+
return value;
|
|
134
|
+
}
|
|
135
|
+
if (typeof value === 'number') {
|
|
136
|
+
if (!Number.isFinite(value))
|
|
137
|
+
throw new Error('Preview inputs must be finite.');
|
|
138
|
+
return value;
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(value))
|
|
141
|
+
return value.map(canonicalize);
|
|
142
|
+
if (value && typeof value === 'object') {
|
|
143
|
+
return Object.fromEntries(Object.entries(value)
|
|
144
|
+
.filter(([, entry]) => entry !== undefined)
|
|
145
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
146
|
+
.map(([key, entry]) => [key, canonicalize(entry)]));
|
|
147
|
+
}
|
|
148
|
+
throw new Error(`Unsupported preview input type '${typeof value}'.`);
|
|
149
|
+
}
|
|
150
|
+
function assertTimestamp(value, label) {
|
|
151
|
+
if (!Number.isFinite(Date.parse(value))) {
|
|
152
|
+
throw new Error(`Invalid ${label} time '${value}'.`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ProviderAdapter, type SanitizedProviderObservation, type SemanticAction } from './provider.js';
|
|
2
|
+
export interface ProviderConformanceFixture {
|
|
3
|
+
adapter: ProviderAdapter;
|
|
4
|
+
observation: SanitizedProviderObservation;
|
|
5
|
+
expected: {
|
|
6
|
+
stableId: string;
|
|
7
|
+
aliases: string[];
|
|
8
|
+
verificationFields: string[];
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function evaluateProviderConformance(fixture: ProviderConformanceFixture): string[];
|
|
12
|
+
export declare function validateSemanticObservation(action: SemanticAction, observation: SanitizedProviderObservation): string[];
|
|
13
|
+
//# sourceMappingURL=provider-conformance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider-conformance.d.ts","sourceRoot":"","sources":["../../../../src/commands/pjm/remote/provider-conformance.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,4BAA4B,EACjC,KAAK,cAAc,EACpB,MAAM,YAAY,CAAC;AAEpB,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,eAAe,CAAC;IACzB,WAAW,EAAE,4BAA4B,CAAC;IAC1C,QAAQ,EAAE;QACR,QAAQ,EAAE,MAAM,CAAC;QACjB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,kBAAkB,EAAE,MAAM,EAAE,CAAC;KAC9B,CAAC;CACH;AAED,wBAAgB,2BAA2B,CACzC,OAAO,EAAE,0BAA0B,GAClC,MAAM,EAAE,CAkCV;AAED,wBAAgB,2BAA2B,CACzC,MAAM,EAAE,cAAc,EACtB,WAAW,EAAE,4BAA4B,GACxC,MAAM,EAAE,CASV"}
|