deepdebug-local-agent 1.3.0 → 1.4.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.
Files changed (108) hide show
  1. package/.github/workflows/deploy-gcp.yml +58 -26
  2. package/CHANGELOG.md +6 -0
  3. package/docs/la-protocol-v2.md +68 -0
  4. package/package.json +2 -1
  5. package/scripts/sync-la-protocol-v2-contract.mjs +58 -0
  6. package/src/la-protocol-v2/chunks.js +45 -0
  7. package/src/la-protocol-v2/config.js +27 -0
  8. package/src/la-protocol-v2/content.js +57 -0
  9. package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +81 -0
  10. package/src/la-protocol-v2/contract/README.md +103 -0
  11. package/src/la-protocol-v2/contract/ack.schema.json +51 -0
  12. package/src/la-protocol-v2/contract/alert.schema.json +62 -0
  13. package/src/la-protocol-v2/contract/approval.decision.schema.json +57 -0
  14. package/src/la-protocol-v2/contract/audit.event.schema.json +86 -0
  15. package/src/la-protocol-v2/contract/config.update.schema.json +73 -0
  16. package/src/la-protocol-v2/contract/content.request.schema.json +110 -0
  17. package/src/la-protocol-v2/contract/content.response.schema.json +90 -0
  18. package/src/la-protocol-v2/contract/defs.schema.json +191 -0
  19. package/src/la-protocol-v2/contract/envelope.schema.json +89 -0
  20. package/src/la-protocol-v2/contract/error.schema.json +59 -0
  21. package/src/la-protocol-v2/contract/heartbeat.schema.json +100 -0
  22. package/src/la-protocol-v2/contract/hello.schema.json +111 -0
  23. package/src/la-protocol-v2/contract/incident.candidate.schema.json +102 -0
  24. package/src/la-protocol-v2/contract/index.json +166 -0
  25. package/src/la-protocol-v2/contract/job.assign.schema.json +212 -0
  26. package/src/la-protocol-v2/contract/job.cancel.schema.json +52 -0
  27. package/src/la-protocol-v2/contract/job.event.schema.json +115 -0
  28. package/src/la-protocol-v2/contract/job.result.schema.json +100 -0
  29. package/src/la-protocol-v2/contract/log.stats.schema.json +79 -0
  30. package/src/la-protocol-v2/contract/ready.schema.json +106 -0
  31. package/src/la-protocol-v2/contract/register.schema.json +92 -0
  32. package/src/la-protocol-v2/contract/scan.upload.schema.json +59 -0
  33. package/src/la-protocol-v2/contract/scan.uploaded.schema.json +51 -0
  34. package/src/la-protocol-v2/contract/webhook.forward.schema.json +69 -0
  35. package/src/la-protocol-v2/contract.js +90 -0
  36. package/src/la-protocol-v2/errors.js +34 -0
  37. package/src/la-protocol-v2/index.js +8 -0
  38. package/src/la-protocol-v2/link.js +93 -0
  39. package/src/la-protocol-v2/runtime.js +72 -0
  40. package/src/la-protocol-v2/session.js +650 -0
  41. package/src/la-protocol-v2/strict-json.js +47 -0
  42. package/src/server.js +22 -4
  43. package/test/contract/la-protocol-v2/vectors/ack.vectors.json +149 -0
  44. package/test/contract/la-protocol-v2/vectors/alert.vectors.json +101 -0
  45. package/test/contract/la-protocol-v2/vectors/approval.decision.vectors.json +165 -0
  46. package/test/contract/la-protocol-v2/vectors/audit.event.vectors.json +156 -0
  47. package/test/contract/la-protocol-v2/vectors/config.update.vectors.json +189 -0
  48. package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +325 -0
  49. package/test/contract/la-protocol-v2/vectors/content.response.vectors.json +413 -0
  50. package/test/contract/la-protocol-v2/vectors/cross-type.vectors.json +260 -0
  51. package/test/contract/la-protocol-v2/vectors/error.vectors.json +179 -0
  52. package/test/contract/la-protocol-v2/vectors/heartbeat.vectors.json +109 -0
  53. package/test/contract/la-protocol-v2/vectors/hello.vectors.json +224 -0
  54. package/test/contract/la-protocol-v2/vectors/incident.candidate.vectors.json +186 -0
  55. package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +631 -0
  56. package/test/contract/la-protocol-v2/vectors/job.cancel.vectors.json +133 -0
  57. package/test/contract/la-protocol-v2/vectors/job.event.vectors.json +233 -0
  58. package/test/contract/la-protocol-v2/vectors/job.result.vectors.json +137 -0
  59. package/test/contract/la-protocol-v2/vectors/log.stats.vectors.json +137 -0
  60. package/test/contract/la-protocol-v2/vectors/ready.vectors.json +177 -0
  61. package/test/contract/la-protocol-v2/vectors/register.vectors.json +399 -0
  62. package/test/contract/la-protocol-v2/vectors/scan.upload.vectors.json +137 -0
  63. package/test/contract/la-protocol-v2/vectors/scan.uploaded.vectors.json +101 -0
  64. package/test/contract/la-protocol-v2/vectors/traces/invalid-agent-mismatch.json +140 -0
  65. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-after-last.json +200 -0
  66. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-byte-length-mismatch.json +174 -0
  67. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-last-flag.json +200 -0
  68. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-out-of-order.json +200 -0
  69. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-total-changes.json +200 -0
  70. package/test/contract/la-protocol-v2/vectors/traces/invalid-clock-skew.json +46 -0
  71. package/test/contract/la-protocol-v2/vectors/traces/invalid-event-before-ready.json +72 -0
  72. package/test/contract/la-protocol-v2/vectors/traces/invalid-first-frame-not-register.json +48 -0
  73. package/test/contract/la-protocol-v2/vectors/traces/invalid-frame-over-48kib.json +141 -0
  74. package/test/contract/la-protocol-v2/vectors/traces/invalid-la-sends-job-assign.json +173 -0
  75. package/test/contract/la-protocol-v2/vectors/traces/invalid-params-raw-differs-from-payload.json +172 -0
  76. package/test/contract/la-protocol-v2/vectors/traces/invalid-payload-above-limit.json +174 -0
  77. package/test/contract/la-protocol-v2/vectors/traces/invalid-resume-restarts-seq.json +144 -0
  78. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-gap.json +166 -0
  79. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-repeated.json +166 -0
  80. package/test/contract/la-protocol-v2/vectors/traces/invalid-sig-tenant-mismatch.json +172 -0
  81. package/test/contract/la-protocol-v2/vectors/traces/invalid-signature-warn-with-enforce-required.json +77 -0
  82. package/test/contract/la-protocol-v2/vectors/traces/invalid-tenant-mismatch.json +140 -0
  83. package/test/contract/la-protocol-v2/vectors/traces/invalid-type-outside-allowlist.json +140 -0
  84. package/test/contract/la-protocol-v2/vectors/traces/valid-content-chunks-in-order.json +224 -0
  85. package/test/contract/la-protocol-v2/vectors/traces/valid-handshake-and-job.json +266 -0
  86. package/test/contract/la-protocol-v2/vectors/traces/valid-resume-continues-seq.json +168 -0
  87. package/test/contract/la-protocol-v2/vectors/traces/valid-retransmit-is-idempotent.json +193 -0
  88. package/test/contract/la-protocol-v2/vectors/webhook.forward.vectors.json +170 -0
  89. package/test/la-protocol-v2/contract.test.js +131 -0
  90. package/test/la-protocol-v2/helpers/la-rig.js +83 -0
  91. package/test/la-protocol-v2/helpers/reference-cp.js +349 -0
  92. package/test/la-protocol-v2/helpers/spawn-agent.js +35 -0
  93. package/test/la-protocol-v2/link.test.js +141 -0
  94. package/test/la-protocol-v2/session-inbound.test.js +279 -0
  95. package/test/la-protocol-v2/session-outbound.test.js +382 -0
  96. package/test/la-protocol-v2/traces.test.js +270 -0
  97. package/test/la-protocol-v2/tunnel-e2e.test.js +124 -0
  98. package/tools/graphify-validation/extras/README.md +19 -0
  99. package/tools/graphify-validation/extras/bymethod.py +35 -0
  100. package/tools/graphify-validation/extras/docker/Dockerfile +16 -0
  101. package/tools/graphify-validation/extras/docker/inner.sh +7 -0
  102. package/tools/graphify-validation/extras/g10.py +59 -0
  103. package/tools/graphify-validation/extras/g3aug.py +49 -0
  104. package/tools/graphify-validation/extras/hybrid.py +34 -0
  105. package/tools/graphify-validation/extras/truth2.py +44 -0
  106. package/tools/interop/LaV2NodeInteropTest.java +266 -0
  107. package/tools/interop/README.md +21 -0
  108. package/tools/interop/la-interop-client.mjs +97 -0
@@ -0,0 +1,47 @@
1
+ /**
2
+ * strict-json.js - JSON.parse that refuses duplicate object keys (the gateway refuses them too: with a
3
+ * duplicate, two parsers could disagree about which value a signed field has).
4
+ *
5
+ * parseStrict(text) -> value. Throws on invalid JSON or on a duplicate key at any depth.
6
+ * Implemented as a small scanner for the keys plus the native parser for the values, so the parsed
7
+ * result is exactly what JSON.parse returns.
8
+ */
9
+ export function parseStrict(text) {
10
+ const value = JSON.parse(text); // throws on invalid JSON
11
+ assertNoDuplicateKeys(text);
12
+ return value;
13
+ }
14
+
15
+ function assertNoDuplicateKeys(text) {
16
+ const stack = []; // one Set of keys per open object; null for arrays
17
+ let i = 0;
18
+ const n = text.length;
19
+ let expectKey = false;
20
+ while (i < n) {
21
+ const c = text.charCodeAt(i);
22
+ if (c === 0x22 /* " */) {
23
+ const start = i + 1;
24
+ i++;
25
+ while (i < n) {
26
+ const d = text.charCodeAt(i);
27
+ if (d === 0x5c /* \ */) { i += 2; continue; }
28
+ if (d === 0x22) break;
29
+ i++;
30
+ }
31
+ const top = stack[stack.length - 1];
32
+ if (expectKey && top) {
33
+ const key = JSON.parse(`"${text.slice(start, i)}"`);
34
+ if (top.has(key)) throw new Error("duplicate key");
35
+ top.add(key);
36
+ expectKey = false;
37
+ }
38
+ i++;
39
+ continue;
40
+ }
41
+ if (c === 0x7b /* { */) { stack.push(new Set()); expectKey = true; }
42
+ else if (c === 0x5b /* [ */) { stack.push(null); expectKey = false; }
43
+ else if (c === 0x7d || c === 0x5d) { stack.pop(); expectKey = false; }
44
+ else if (c === 0x2c /* , */) { expectKey = stack.length > 0 && stack[stack.length - 1] !== null; }
45
+ i++;
46
+ }
47
+ }
package/src/server.js CHANGED
@@ -19,6 +19,7 @@ import { getSigningConfig, createSignatureMiddleware, captureRawBody, describeSi
19
19
  import { decideExec, rejectionBody, resolveExecPolicyConfig } from "./utils/exec-policy.js";
20
20
  import { createHash as wsCreateHash } from "node:crypto";
21
21
  import { resolveWsMetadataConfig, createWsMetadataGuard, createConnectionState, createOfflinePolicyGuard } from "./ws-channel/index.js";
22
+ import { resolveProtocolV2Config, resolveAgentId } from "./la-protocol-v2/config.js";
22
23
  import { resolveEffectiveHeaderRoot } from "./utils/root-recovery.js";
23
24
  import { classifyGrepFailure, partialSearchBody } from "./utils/search-outcome.js";
24
25
  import { classifyOpenDestination, extractOpenTenantId, isSafeToRemove } from "./utils/open-gate.js";
@@ -644,6 +645,8 @@ console.log(`[exec-policy] mode=${EXEC_POLICY.mode} (allowlist; LOCAL_AGENT_EXEC
644
645
  // actions are denied while the control plane is unreachable. off = nothing changes.
645
646
  const WS_METADATA = resolveWsMetadataConfig();
646
647
  const WS_CONNECTION = createConnectionState();
648
+ // LOCAL_AGENT_PROTOCOL_V2 (F3-05b, default off): with it off the tunnel below is the v1 client, untouched.
649
+ const PROTOCOL_V2 = resolveProtocolV2Config();
647
650
  if (WS_METADATA.mode === "enforce") {
648
651
  EXEC_POLICY.offlineGuard = createOfflinePolicyGuard({
649
652
  connection: WS_CONNECTION,
@@ -6941,7 +6944,7 @@ app.post("/workspace/:workspaceId/run", async (req, res) => {
6941
6944
  // WEBSOCKET REVERSE TUNNEL
6942
6945
  // Connects Local Agent to Gateway no public URL needed
6943
6946
  // ============================================
6944
- async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
6947
+ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId, agentId = null) {
6945
6948
  if (!gatewayUrl || !tenantId) {
6946
6949
  console.log('WebSocket tunnel skipped: missing gatewayUrl or tenantId');
6947
6950
  return;
@@ -6977,6 +6980,16 @@ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
6977
6980
  const out = metaGuard ? guardedFrame : JSON.stringify(legacyObj);
6978
6981
  if (out) ws.send(out);
6979
6982
  };
6983
+ // Protocol v2 (F3-05b): only when LOCAL_AGENT_PROTOCOL_V2=true. null = the v1 client exactly as before.
6984
+ let v2Link = null;
6985
+ if (PROTOCOL_V2.enabled) {
6986
+ try {
6987
+ const { buildV2Link } = await import('./la-protocol-v2/runtime.js');
6988
+ v2Link = buildV2Link({ tenantId, agentId, signing: SIGNING, wsManager, getWorkspaceRoot: () => WORKSPACE_ROOT });
6989
+ } catch (err) {
6990
+ console.warn(`[la-v2] off: ${err.code || err.name} while loading the protocol module`);
6991
+ }
6992
+ }
6980
6993
 
6981
6994
  async function connect() {
6982
6995
  if (isShuttingDown) return;
@@ -6989,10 +7002,12 @@ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
6989
7002
  reconnectDelay = 2000;
6990
7003
  WS_CONNECTION.markOpen();
6991
7004
  const legacyRegister = { type: 'register', tenantId, port, workspacePath: WORKSPACE_ROOT || null };
6992
- sendFrame(legacyRegister, metaGuard && metaGuard.frameMessage({
7005
+ const sendLegacyRegister = () => sendFrame(legacyRegister, metaGuard && metaGuard.frameMessage({
6993
7006
  type: 'register', tenantId, port: Number(port),
6994
7007
  ...(WORKSPACE_ROOT ? { workspaceHash: wsCreateHash("sha256").update(String(WORKSPACE_ROOT)).digest('hex') } : {}),
6995
7008
  }));
7009
+ // v2 owns the registration while it negotiates; the v1 register goes out only if the gateway does not speak v2.
7010
+ if (!(v2Link && v2Link.begin(ws, sendLegacyRegister))) sendLegacyRegister();
6996
7011
  const hb = setInterval(() => {
6997
7012
  if (ws.readyState === WebSocket.OPEN) {
6998
7013
  ws.send('ping');
@@ -7005,10 +7020,12 @@ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
7005
7020
 
7006
7021
  ws.on('pong', () => WS_CONNECTION.markSeen());
7007
7022
 
7008
- ws.on('message', async (data) => {
7023
+ ws.on('message', async (data, isBinary) => {
7009
7024
  WS_CONNECTION.markSeen();
7010
7025
  const text = data.toString();
7011
7026
  if (text === 'pong') return;
7027
+ // v2 frames (schemaVersion) are handled by the protocol module; v1 commands fall through to the handler below.
7028
+ if (v2Link && v2Link.handleMessage(ws, text, isBinary)) return;
7012
7029
  let request;
7013
7030
  try { request = JSON.parse(text); } catch { return; }
7014
7031
  const { requestId, command } = request;
@@ -7035,6 +7052,7 @@ async function startWebSocketTunnel(port, gatewayUrl, authToken, tenantId) {
7035
7052
 
7036
7053
  ws.on('close', () => {
7037
7054
  WS_CONNECTION.markClosed();
7055
+ if (v2Link) v2Link.onClose(ws);
7038
7056
  if (!isShuttingDown) {
7039
7057
  console.log(`WS disconnected. Reconnecting in ${reconnectDelay/1000}s...`);
7040
7058
  setTimeout(connect, reconnectDelay);
@@ -8164,7 +8182,7 @@ app.listen(PORT, BIND_HOST, async () => {
8164
8182
  }
8165
8183
 
8166
8184
  if (process.env.DISABLE_WS_TUNNEL !== 'true') {
8167
- startWebSocketTunnel(PORT, gwUrl, wsToken, tenantId);
8185
+ startWebSocketTunnel(PORT, gwUrl, wsToken, tenantId, resolveAgentId(process.env, cfg));
8168
8186
  }
8169
8187
 
8170
8188
  // Sibling plane (started here so the frozen monolith is untouched): metrics
@@ -0,0 +1,149 @@
1
+ {
2
+ "type": "ack",
3
+ "schema": "ack.schema.json",
4
+ "vectors": [
5
+ {
6
+ "name": "minimal-valid",
7
+ "expect": {
8
+ "valid": true
9
+ },
10
+ "message": {
11
+ "schemaVersion": 2,
12
+ "type": "ack",
13
+ "msgId": "00000000-0000-4000-8000-000000000001",
14
+ "seq": 0,
15
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
16
+ "workspaceId": "ws-1",
17
+ "ts": "2026-10-29T10:00:00Z",
18
+ "payload": {
19
+ "ackSeq": 1,
20
+ "status": "accepted"
21
+ },
22
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
23
+ }
24
+ },
25
+ {
26
+ "name": "extra-top-level-field",
27
+ "why": "envelope is closed (additionalProperties:false)",
28
+ "expect": {
29
+ "valid": false,
30
+ "code": "schema_violation",
31
+ "keyword": "additionalProperties"
32
+ },
33
+ "message": {
34
+ "schemaVersion": 2,
35
+ "type": "ack",
36
+ "msgId": "00000000-0000-4000-8000-000000000001",
37
+ "seq": 0,
38
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
39
+ "workspaceId": "ws-1",
40
+ "ts": "2026-10-29T10:00:00Z",
41
+ "payload": {
42
+ "ackSeq": 1,
43
+ "status": "accepted"
44
+ },
45
+ "extra": "x",
46
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
47
+ }
48
+ },
49
+ {
50
+ "name": "extra-payload-field",
51
+ "why": "payload is closed: no free-form fields",
52
+ "expect": {
53
+ "valid": false,
54
+ "code": "schema_violation",
55
+ "keyword": "additionalProperties"
56
+ },
57
+ "message": {
58
+ "schemaVersion": 2,
59
+ "type": "ack",
60
+ "msgId": "00000000-0000-4000-8000-000000000001",
61
+ "seq": 0,
62
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
63
+ "workspaceId": "ws-1",
64
+ "ts": "2026-10-29T10:00:00Z",
65
+ "payload": {
66
+ "ackSeq": 1,
67
+ "status": "accepted",
68
+ "freeText": "client text"
69
+ },
70
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
71
+ }
72
+ },
73
+ {
74
+ "name": "tenant-id-not-uuid",
75
+ "why": "tenantId must be a UUID",
76
+ "expect": {
77
+ "valid": false,
78
+ "code": "schema_violation",
79
+ "keyword": "pattern"
80
+ },
81
+ "message": {
82
+ "schemaVersion": 2,
83
+ "type": "ack",
84
+ "msgId": "00000000-0000-4000-8000-000000000001",
85
+ "seq": 0,
86
+ "tenantId": "tenant-1",
87
+ "workspaceId": "ws-1",
88
+ "ts": "2026-10-29T10:00:00Z",
89
+ "payload": {
90
+ "ackSeq": 1,
91
+ "status": "accepted"
92
+ },
93
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
94
+ }
95
+ },
96
+ {
97
+ "name": "ack-with-sig",
98
+ "why": "control frames are not signed",
99
+ "expect": {
100
+ "valid": false,
101
+ "code": "schema_violation"
102
+ },
103
+ "message": {
104
+ "schemaVersion": 2,
105
+ "type": "ack",
106
+ "msgId": "00000000-0000-4000-8000-000000000001",
107
+ "seq": 0,
108
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
109
+ "workspaceId": "ws-1",
110
+ "ts": "2026-10-29T10:00:00Z",
111
+ "payload": {
112
+ "ackSeq": 1,
113
+ "status": "accepted"
114
+ },
115
+ "sig": {
116
+ "keyId": "vh-cp-2026-10",
117
+ "timestamp": "1793268000",
118
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
119
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
120
+ "workspaceId": "ws-1",
121
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
122
+ },
123
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
124
+ }
125
+ },
126
+ {
127
+ "name": "missing-agent-id",
128
+ "expect": {
129
+ "valid": false,
130
+ "code": "schema_violation",
131
+ "keyword": "required"
132
+ },
133
+ "message": {
134
+ "schemaVersion": 2,
135
+ "type": "ack",
136
+ "msgId": "00000000-0000-4000-8000-000000000001",
137
+ "seq": 0,
138
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
139
+ "workspaceId": "ws-1",
140
+ "ts": "2026-10-29T10:00:00Z",
141
+ "payload": {
142
+ "ackSeq": 1,
143
+ "status": "accepted"
144
+ }
145
+ },
146
+ "why": "agentId is required on ack/error (the gateway refuses a frame without it: agent_mismatch)"
147
+ }
148
+ ]
149
+ }
@@ -0,0 +1,101 @@
1
+ {
2
+ "type": "alert",
3
+ "schema": "alert.schema.json",
4
+ "vectors": [
5
+ {
6
+ "name": "minimal-valid",
7
+ "expect": {
8
+ "valid": true
9
+ },
10
+ "message": {
11
+ "schemaVersion": 2,
12
+ "type": "alert",
13
+ "msgId": "00000000-0000-4000-8000-000000000001",
14
+ "seq": 1,
15
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
16
+ "workspaceId": "ws-1",
17
+ "ts": "2026-10-29T10:00:00Z",
18
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
19
+ "payload": {
20
+ "code": "index_stale",
21
+ "severity": "warning",
22
+ "value": 86400
23
+ }
24
+ }
25
+ },
26
+ {
27
+ "name": "extra-top-level-field",
28
+ "why": "envelope is closed (additionalProperties:false)",
29
+ "expect": {
30
+ "valid": false,
31
+ "code": "schema_violation",
32
+ "keyword": "additionalProperties"
33
+ },
34
+ "message": {
35
+ "schemaVersion": 2,
36
+ "type": "alert",
37
+ "msgId": "00000000-0000-4000-8000-000000000001",
38
+ "seq": 1,
39
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
40
+ "workspaceId": "ws-1",
41
+ "ts": "2026-10-29T10:00:00Z",
42
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
43
+ "payload": {
44
+ "code": "index_stale",
45
+ "severity": "warning",
46
+ "value": 86400
47
+ },
48
+ "extra": "x"
49
+ }
50
+ },
51
+ {
52
+ "name": "extra-payload-field",
53
+ "why": "payload is closed: no free-form fields",
54
+ "expect": {
55
+ "valid": false,
56
+ "code": "schema_violation",
57
+ "keyword": "additionalProperties"
58
+ },
59
+ "message": {
60
+ "schemaVersion": 2,
61
+ "type": "alert",
62
+ "msgId": "00000000-0000-4000-8000-000000000001",
63
+ "seq": 1,
64
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
65
+ "workspaceId": "ws-1",
66
+ "ts": "2026-10-29T10:00:00Z",
67
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
68
+ "payload": {
69
+ "code": "index_stale",
70
+ "severity": "warning",
71
+ "value": 86400,
72
+ "freeText": "client text"
73
+ }
74
+ }
75
+ },
76
+ {
77
+ "name": "tenant-id-not-uuid",
78
+ "why": "tenantId must be a UUID",
79
+ "expect": {
80
+ "valid": false,
81
+ "code": "schema_violation",
82
+ "keyword": "pattern"
83
+ },
84
+ "message": {
85
+ "schemaVersion": 2,
86
+ "type": "alert",
87
+ "msgId": "00000000-0000-4000-8000-000000000001",
88
+ "seq": 1,
89
+ "tenantId": "tenant-1",
90
+ "workspaceId": "ws-1",
91
+ "ts": "2026-10-29T10:00:00Z",
92
+ "agentId": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
93
+ "payload": {
94
+ "code": "index_stale",
95
+ "severity": "warning",
96
+ "value": 86400
97
+ }
98
+ }
99
+ }
100
+ ]
101
+ }
@@ -0,0 +1,165 @@
1
+ {
2
+ "type": "approval.decision",
3
+ "schema": "approval.decision.schema.json",
4
+ "vectors": [
5
+ {
6
+ "name": "minimal-valid",
7
+ "expect": {
8
+ "valid": true
9
+ },
10
+ "message": {
11
+ "schemaVersion": 2,
12
+ "type": "approval.decision",
13
+ "msgId": "00000000-0000-4000-8000-000000000001",
14
+ "seq": 1,
15
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
16
+ "workspaceId": "ws-1",
17
+ "ts": "2026-10-29T10:00:00Z",
18
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
19
+ "sig": {
20
+ "keyId": "vh-cp-2026-10",
21
+ "timestamp": "1793268000",
22
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
23
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
24
+ "workspaceId": "ws-1",
25
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
26
+ },
27
+ "payload": {
28
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
29
+ "decision": "approved",
30
+ "token": "vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl"
31
+ }
32
+ }
33
+ },
34
+ {
35
+ "name": "extra-top-level-field",
36
+ "why": "envelope is closed (additionalProperties:false)",
37
+ "expect": {
38
+ "valid": false,
39
+ "code": "schema_violation",
40
+ "keyword": "additionalProperties"
41
+ },
42
+ "message": {
43
+ "schemaVersion": 2,
44
+ "type": "approval.decision",
45
+ "msgId": "00000000-0000-4000-8000-000000000001",
46
+ "seq": 1,
47
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
48
+ "workspaceId": "ws-1",
49
+ "ts": "2026-10-29T10:00:00Z",
50
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
51
+ "sig": {
52
+ "keyId": "vh-cp-2026-10",
53
+ "timestamp": "1793268000",
54
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
55
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
56
+ "workspaceId": "ws-1",
57
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
58
+ },
59
+ "payload": {
60
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
61
+ "decision": "approved",
62
+ "token": "vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl"
63
+ },
64
+ "extra": "x"
65
+ }
66
+ },
67
+ {
68
+ "name": "extra-payload-field",
69
+ "why": "payload is closed: no free-form fields",
70
+ "expect": {
71
+ "valid": false,
72
+ "code": "schema_violation",
73
+ "keyword": "additionalProperties"
74
+ },
75
+ "message": {
76
+ "schemaVersion": 2,
77
+ "type": "approval.decision",
78
+ "msgId": "00000000-0000-4000-8000-000000000001",
79
+ "seq": 1,
80
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
81
+ "workspaceId": "ws-1",
82
+ "ts": "2026-10-29T10:00:00Z",
83
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
84
+ "sig": {
85
+ "keyId": "vh-cp-2026-10",
86
+ "timestamp": "1793268000",
87
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
88
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
89
+ "workspaceId": "ws-1",
90
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
91
+ },
92
+ "payload": {
93
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
94
+ "decision": "approved",
95
+ "token": "vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl",
96
+ "freeText": "client text"
97
+ }
98
+ }
99
+ },
100
+ {
101
+ "name": "tenant-id-not-uuid",
102
+ "why": "tenantId must be a UUID",
103
+ "expect": {
104
+ "valid": false,
105
+ "code": "schema_violation",
106
+ "keyword": "pattern"
107
+ },
108
+ "message": {
109
+ "schemaVersion": 2,
110
+ "type": "approval.decision",
111
+ "msgId": "00000000-0000-4000-8000-000000000001",
112
+ "seq": 1,
113
+ "tenantId": "tenant-1",
114
+ "workspaceId": "ws-1",
115
+ "ts": "2026-10-29T10:00:00Z",
116
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
117
+ "sig": {
118
+ "keyId": "vh-cp-2026-10",
119
+ "timestamp": "1793268000",
120
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
121
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
122
+ "workspaceId": "ws-1",
123
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
124
+ },
125
+ "payload": {
126
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
127
+ "decision": "approved",
128
+ "token": "vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl"
129
+ }
130
+ }
131
+ },
132
+ {
133
+ "name": "token-not-vhapr1",
134
+ "why": "token must be a vhapr1 token",
135
+ "expect": {
136
+ "valid": false,
137
+ "code": "schema_violation",
138
+ "keyword": "pattern"
139
+ },
140
+ "message": {
141
+ "schemaVersion": 2,
142
+ "type": "approval.decision",
143
+ "msgId": "00000000-0000-4000-8000-000000000001",
144
+ "seq": 1,
145
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
146
+ "workspaceId": "ws-1",
147
+ "ts": "2026-10-29T10:00:00Z",
148
+ "paramsRaw": "{\"approvalId\":\"d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80\",\"decision\":\"approved\",\"token\":\"vhapr1.k1.eyJ0eXAiOiJ2aC1odW1hbi1hcHByb3ZhbCJ9.c2lnbmF0dXJl\"}",
149
+ "sig": {
150
+ "keyId": "vh-cp-2026-10",
151
+ "timestamp": "1793268000",
152
+ "nonce": "Zm9vYmFyYmF6cXV4MTIzNDU2",
153
+ "tenantId": "3f2b8c1e-5a4d-4e7b-9c21-0a1b2c3d4e5f",
154
+ "workspaceId": "ws-1",
155
+ "signature": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=="
156
+ },
157
+ "payload": {
158
+ "approvalId": "d4e5f6a7-b8c9-4d0e-9f1a-3b4c5d6e7f80",
159
+ "decision": "approved",
160
+ "token": "bearer abc"
161
+ }
162
+ }
163
+ }
164
+ ]
165
+ }