deepdebug-local-agent 1.3.0 → 1.5.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 (123) hide show
  1. package/.github/workflows/deploy-gcp.yml +58 -26
  2. package/CHANGELOG.md +6 -0
  3. package/docs/la-protocol-v2.md +102 -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-handlers/audit.js +36 -0
  9. package/src/la-protocol-v2/content-handlers/config.js +48 -0
  10. package/src/la-protocol-v2/content-handlers/egress.js +65 -0
  11. package/src/la-protocol-v2/content-handlers/git-diff.js +92 -0
  12. package/src/la-protocol-v2/content-handlers/handler.js +170 -0
  13. package/src/la-protocol-v2/content-handlers/index.js +6 -0
  14. package/src/la-protocol-v2/content-handlers/install.js +50 -0
  15. package/src/la-protocol-v2/content-handlers/path-guard.js +142 -0
  16. package/src/la-protocol-v2/content-handlers/ref-registry.js +87 -0
  17. package/src/la-protocol-v2/content.js +63 -0
  18. package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +81 -0
  19. package/src/la-protocol-v2/contract/README.md +103 -0
  20. package/src/la-protocol-v2/contract/ack.schema.json +51 -0
  21. package/src/la-protocol-v2/contract/alert.schema.json +62 -0
  22. package/src/la-protocol-v2/contract/approval.decision.schema.json +57 -0
  23. package/src/la-protocol-v2/contract/audit.event.schema.json +86 -0
  24. package/src/la-protocol-v2/contract/config.update.schema.json +73 -0
  25. package/src/la-protocol-v2/contract/content.request.schema.json +110 -0
  26. package/src/la-protocol-v2/contract/content.response.schema.json +90 -0
  27. package/src/la-protocol-v2/contract/defs.schema.json +191 -0
  28. package/src/la-protocol-v2/contract/envelope.schema.json +89 -0
  29. package/src/la-protocol-v2/contract/error.schema.json +59 -0
  30. package/src/la-protocol-v2/contract/heartbeat.schema.json +100 -0
  31. package/src/la-protocol-v2/contract/hello.schema.json +111 -0
  32. package/src/la-protocol-v2/contract/incident.candidate.schema.json +102 -0
  33. package/src/la-protocol-v2/contract/index.json +166 -0
  34. package/src/la-protocol-v2/contract/job.assign.schema.json +212 -0
  35. package/src/la-protocol-v2/contract/job.cancel.schema.json +52 -0
  36. package/src/la-protocol-v2/contract/job.event.schema.json +115 -0
  37. package/src/la-protocol-v2/contract/job.result.schema.json +100 -0
  38. package/src/la-protocol-v2/contract/log.stats.schema.json +79 -0
  39. package/src/la-protocol-v2/contract/ready.schema.json +106 -0
  40. package/src/la-protocol-v2/contract/register.schema.json +92 -0
  41. package/src/la-protocol-v2/contract/scan.upload.schema.json +59 -0
  42. package/src/la-protocol-v2/contract/scan.uploaded.schema.json +51 -0
  43. package/src/la-protocol-v2/contract/webhook.forward.schema.json +69 -0
  44. package/src/la-protocol-v2/contract.js +90 -0
  45. package/src/la-protocol-v2/errors.js +34 -0
  46. package/src/la-protocol-v2/index.js +8 -0
  47. package/src/la-protocol-v2/link.js +93 -0
  48. package/src/la-protocol-v2/runtime.js +81 -0
  49. package/src/la-protocol-v2/session.js +650 -0
  50. package/src/la-protocol-v2/strict-json.js +47 -0
  51. package/src/server.js +25 -4
  52. package/test/contract/la-protocol-v2/vectors/ack.vectors.json +149 -0
  53. package/test/contract/la-protocol-v2/vectors/alert.vectors.json +101 -0
  54. package/test/contract/la-protocol-v2/vectors/approval.decision.vectors.json +165 -0
  55. package/test/contract/la-protocol-v2/vectors/audit.event.vectors.json +156 -0
  56. package/test/contract/la-protocol-v2/vectors/config.update.vectors.json +189 -0
  57. package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +325 -0
  58. package/test/contract/la-protocol-v2/vectors/content.response.vectors.json +413 -0
  59. package/test/contract/la-protocol-v2/vectors/cross-type.vectors.json +260 -0
  60. package/test/contract/la-protocol-v2/vectors/error.vectors.json +179 -0
  61. package/test/contract/la-protocol-v2/vectors/heartbeat.vectors.json +109 -0
  62. package/test/contract/la-protocol-v2/vectors/hello.vectors.json +224 -0
  63. package/test/contract/la-protocol-v2/vectors/incident.candidate.vectors.json +186 -0
  64. package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +631 -0
  65. package/test/contract/la-protocol-v2/vectors/job.cancel.vectors.json +133 -0
  66. package/test/contract/la-protocol-v2/vectors/job.event.vectors.json +233 -0
  67. package/test/contract/la-protocol-v2/vectors/job.result.vectors.json +137 -0
  68. package/test/contract/la-protocol-v2/vectors/log.stats.vectors.json +137 -0
  69. package/test/contract/la-protocol-v2/vectors/ready.vectors.json +177 -0
  70. package/test/contract/la-protocol-v2/vectors/register.vectors.json +399 -0
  71. package/test/contract/la-protocol-v2/vectors/scan.upload.vectors.json +137 -0
  72. package/test/contract/la-protocol-v2/vectors/scan.uploaded.vectors.json +101 -0
  73. package/test/contract/la-protocol-v2/vectors/traces/invalid-agent-mismatch.json +140 -0
  74. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-after-last.json +200 -0
  75. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-byte-length-mismatch.json +174 -0
  76. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-last-flag.json +200 -0
  77. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-out-of-order.json +200 -0
  78. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-total-changes.json +200 -0
  79. package/test/contract/la-protocol-v2/vectors/traces/invalid-clock-skew.json +46 -0
  80. package/test/contract/la-protocol-v2/vectors/traces/invalid-event-before-ready.json +72 -0
  81. package/test/contract/la-protocol-v2/vectors/traces/invalid-first-frame-not-register.json +48 -0
  82. package/test/contract/la-protocol-v2/vectors/traces/invalid-frame-over-48kib.json +141 -0
  83. package/test/contract/la-protocol-v2/vectors/traces/invalid-la-sends-job-assign.json +173 -0
  84. package/test/contract/la-protocol-v2/vectors/traces/invalid-params-raw-differs-from-payload.json +172 -0
  85. package/test/contract/la-protocol-v2/vectors/traces/invalid-payload-above-limit.json +174 -0
  86. package/test/contract/la-protocol-v2/vectors/traces/invalid-resume-restarts-seq.json +144 -0
  87. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-gap.json +166 -0
  88. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-repeated.json +166 -0
  89. package/test/contract/la-protocol-v2/vectors/traces/invalid-sig-tenant-mismatch.json +172 -0
  90. package/test/contract/la-protocol-v2/vectors/traces/invalid-signature-warn-with-enforce-required.json +77 -0
  91. package/test/contract/la-protocol-v2/vectors/traces/invalid-tenant-mismatch.json +140 -0
  92. package/test/contract/la-protocol-v2/vectors/traces/invalid-type-outside-allowlist.json +140 -0
  93. package/test/contract/la-protocol-v2/vectors/traces/valid-content-chunks-in-order.json +224 -0
  94. package/test/contract/la-protocol-v2/vectors/traces/valid-handshake-and-job.json +266 -0
  95. package/test/contract/la-protocol-v2/vectors/traces/valid-resume-continues-seq.json +168 -0
  96. package/test/contract/la-protocol-v2/vectors/traces/valid-retransmit-is-idempotent.json +193 -0
  97. package/test/contract/la-protocol-v2/vectors/webhook.forward.vectors.json +170 -0
  98. package/test/la-protocol-v2/content-handlers.test.js +633 -0
  99. package/test/la-protocol-v2/content-path-guard.test.js +214 -0
  100. package/test/la-protocol-v2/content-wiring.test.js +225 -0
  101. package/test/la-protocol-v2/contract.test.js +131 -0
  102. package/test/la-protocol-v2/helpers/content-rig.js +94 -0
  103. package/test/la-protocol-v2/helpers/la-rig.js +83 -0
  104. package/test/la-protocol-v2/helpers/reference-cp.js +349 -0
  105. package/test/la-protocol-v2/helpers/spawn-agent.js +36 -0
  106. package/test/la-protocol-v2/link.test.js +141 -0
  107. package/test/la-protocol-v2/session-inbound.test.js +279 -0
  108. package/test/la-protocol-v2/session-outbound.test.js +382 -0
  109. package/test/la-protocol-v2/traces.test.js +270 -0
  110. package/test/la-protocol-v2/tunnel-e2e.test.js +124 -0
  111. package/tools/graphify-validation/extras/README.md +19 -0
  112. package/tools/graphify-validation/extras/bymethod.py +35 -0
  113. package/tools/graphify-validation/extras/docker/Dockerfile +16 -0
  114. package/tools/graphify-validation/extras/docker/inner.sh +7 -0
  115. package/tools/graphify-validation/extras/g10.py +59 -0
  116. package/tools/graphify-validation/extras/g3aug.py +49 -0
  117. package/tools/graphify-validation/extras/hybrid.py +34 -0
  118. package/tools/graphify-validation/extras/truth2.py +44 -0
  119. package/tools/interop/LaV2NodeContentInteropTest.java +248 -0
  120. package/tools/interop/LaV2NodeInteropTest.java +266 -0
  121. package/tools/interop/README.md +38 -0
  122. package/tools/interop/la-interop-client.mjs +97 -0
  123. package/tools/interop/la-interop-content-client.mjs +91 -0
@@ -0,0 +1,79 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.visionhammer.io/la-protocol-v2/log.stats.schema.json",
4
+ "title": "LA protocol v2: log.stats (la->cp)",
5
+ "type": "object",
6
+ "allOf": [
7
+ {
8
+ "$ref": "envelope.schema.json"
9
+ }
10
+ ],
11
+ "properties": {
12
+ "type": {
13
+ "const": "log.stats"
14
+ },
15
+ "payload": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "windowStart": {
20
+ "$ref": "defs.schema.json#/$defs/ts"
21
+ },
22
+ "windowSeconds": {
23
+ "type": "integer",
24
+ "minimum": 1,
25
+ "maximum": 86400
26
+ },
27
+ "counts": {
28
+ "type": "array",
29
+ "maxItems": 64,
30
+ "items": {
31
+ "type": "object",
32
+ "additionalProperties": false,
33
+ "properties": {
34
+ "category": {
35
+ "type": "string",
36
+ "enum": [
37
+ "exception",
38
+ "timeout",
39
+ "http_5xx",
40
+ "db",
41
+ "oom",
42
+ "security",
43
+ "other"
44
+ ]
45
+ },
46
+ "count": {
47
+ "type": "integer",
48
+ "minimum": 0,
49
+ "maximum": 9007199254740991
50
+ }
51
+ },
52
+ "required": [
53
+ "category",
54
+ "count"
55
+ ]
56
+ }
57
+ }
58
+ },
59
+ "required": [
60
+ "windowStart",
61
+ "windowSeconds",
62
+ "counts"
63
+ ]
64
+ },
65
+ "seq": {
66
+ "type": "integer",
67
+ "minimum": 1,
68
+ "maximum": 9007199254740991
69
+ },
70
+ "chunk": false,
71
+ "sig": false,
72
+ "paramsRaw": false,
73
+ "agentId": true
74
+ },
75
+ "required": [
76
+ "payload",
77
+ "agentId"
78
+ ]
79
+ }
@@ -0,0 +1,106 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.visionhammer.io/la-protocol-v2/ready.schema.json",
4
+ "title": "LA protocol v2: ready (cp->la)",
5
+ "type": "object",
6
+ "allOf": [
7
+ {
8
+ "$ref": "envelope.schema.json"
9
+ }
10
+ ],
11
+ "properties": {
12
+ "type": {
13
+ "const": "ready"
14
+ },
15
+ "payload": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "protocol": {
20
+ "const": 2
21
+ },
22
+ "sessionId": {
23
+ "$ref": "defs.schema.json#/$defs/uuid"
24
+ },
25
+ "resumed": {
26
+ "type": "boolean"
27
+ },
28
+ "peerLastSeq": {
29
+ "$ref": "defs.schema.json#/$defs/seq"
30
+ },
31
+ "limits": {
32
+ "type": "object",
33
+ "additionalProperties": false,
34
+ "properties": {
35
+ "maxFrameBytes": {
36
+ "type": "integer",
37
+ "minimum": 1024,
38
+ "maximum": 49152
39
+ },
40
+ "chunkRawBytes": {
41
+ "type": "integer",
42
+ "minimum": 1024,
43
+ "maximum": 32768
44
+ },
45
+ "maxChunks": {
46
+ "type": "integer",
47
+ "minimum": 1,
48
+ "maximum": 64
49
+ },
50
+ "ackTimeoutMs": {
51
+ "type": "integer",
52
+ "minimum": 100,
53
+ "maximum": 60000
54
+ },
55
+ "maxRetries": {
56
+ "type": "integer",
57
+ "minimum": 0,
58
+ "maximum": 3
59
+ },
60
+ "heartbeatSeconds": {
61
+ "type": "integer",
62
+ "minimum": 5,
63
+ "maximum": 300
64
+ },
65
+ "contentFirstByteMs": {
66
+ "type": "integer",
67
+ "minimum": 100,
68
+ "maximum": 60000
69
+ },
70
+ "contentTotalMs": {
71
+ "type": "integer",
72
+ "minimum": 100,
73
+ "maximum": 120000
74
+ }
75
+ },
76
+ "required": [
77
+ "maxFrameBytes",
78
+ "chunkRawBytes",
79
+ "maxChunks",
80
+ "ackTimeoutMs",
81
+ "maxRetries",
82
+ "heartbeatSeconds"
83
+ ]
84
+ }
85
+ },
86
+ "required": [
87
+ "protocol",
88
+ "sessionId",
89
+ "resumed",
90
+ "peerLastSeq",
91
+ "limits"
92
+ ]
93
+ },
94
+ "seq": {
95
+ "const": 0
96
+ },
97
+ "chunk": false,
98
+ "sig": true,
99
+ "paramsRaw": true
100
+ },
101
+ "required": [
102
+ "payload",
103
+ "sig",
104
+ "paramsRaw"
105
+ ]
106
+ }
@@ -0,0 +1,92 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.visionhammer.io/la-protocol-v2/register.schema.json",
4
+ "title": "LA protocol v2: register (la->cp)",
5
+ "type": "object",
6
+ "allOf": [
7
+ {
8
+ "$ref": "envelope.schema.json"
9
+ }
10
+ ],
11
+ "properties": {
12
+ "type": {
13
+ "const": "register"
14
+ },
15
+ "payload": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "protocol": {
20
+ "const": 2
21
+ },
22
+ "laVersion": {
23
+ "$ref": "defs.schema.json#/$defs/semver"
24
+ },
25
+ "capabilities": {
26
+ "type": "array",
27
+ "maxItems": 32,
28
+ "uniqueItems": true,
29
+ "items": {
30
+ "$ref": "defs.schema.json#/$defs/capability"
31
+ }
32
+ },
33
+ "signatureMode": {
34
+ "$ref": "defs.schema.json#/$defs/signatureMode"
35
+ },
36
+ "workspaces": {
37
+ "type": "array",
38
+ "maxItems": 64,
39
+ "items": {
40
+ "type": "object",
41
+ "additionalProperties": false,
42
+ "properties": {
43
+ "workspaceId": {
44
+ "$ref": "defs.schema.json#/$defs/workspaceId"
45
+ },
46
+ "pathSha256": {
47
+ "$ref": "defs.schema.json#/$defs/sha256"
48
+ }
49
+ },
50
+ "required": [
51
+ "workspaceId"
52
+ ]
53
+ }
54
+ },
55
+ "resume": {
56
+ "type": "object",
57
+ "additionalProperties": false,
58
+ "properties": {
59
+ "sessionId": {
60
+ "$ref": "defs.schema.json#/$defs/uuid"
61
+ },
62
+ "lastRecvSeq": {
63
+ "$ref": "defs.schema.json#/$defs/seq"
64
+ }
65
+ },
66
+ "required": [
67
+ "sessionId",
68
+ "lastRecvSeq"
69
+ ]
70
+ }
71
+ },
72
+ "required": [
73
+ "protocol",
74
+ "laVersion",
75
+ "capabilities",
76
+ "signatureMode",
77
+ "workspaces"
78
+ ]
79
+ },
80
+ "seq": {
81
+ "const": 0
82
+ },
83
+ "chunk": false,
84
+ "sig": false,
85
+ "paramsRaw": false,
86
+ "agentId": true
87
+ },
88
+ "required": [
89
+ "payload",
90
+ "agentId"
91
+ ]
92
+ }
@@ -0,0 +1,59 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.visionhammer.io/la-protocol-v2/scan.upload.schema.json",
4
+ "title": "LA protocol v2: scan.upload (cp->la)",
5
+ "type": "object",
6
+ "allOf": [
7
+ {
8
+ "$ref": "envelope.schema.json"
9
+ }
10
+ ],
11
+ "properties": {
12
+ "type": {
13
+ "const": "scan.upload"
14
+ },
15
+ "payload": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "scanId": {
20
+ "$ref": "defs.schema.json#/$defs/uuid"
21
+ },
22
+ "signedPutUrl": {
23
+ "type": "string",
24
+ "minLength": 1,
25
+ "maxLength": 2048,
26
+ "pattern": "^https://[^\\s]{1,2040}$",
27
+ "x-large-metadata": true
28
+ },
29
+ "maxBytes": {
30
+ "type": "integer",
31
+ "minimum": 1,
32
+ "maximum": 5368709120
33
+ },
34
+ "expiresAt": {
35
+ "$ref": "defs.schema.json#/$defs/ts"
36
+ }
37
+ },
38
+ "required": [
39
+ "scanId",
40
+ "signedPutUrl",
41
+ "maxBytes",
42
+ "expiresAt"
43
+ ]
44
+ },
45
+ "seq": {
46
+ "type": "integer",
47
+ "minimum": 1,
48
+ "maximum": 9007199254740991
49
+ },
50
+ "chunk": false,
51
+ "sig": true,
52
+ "paramsRaw": true
53
+ },
54
+ "required": [
55
+ "payload",
56
+ "sig",
57
+ "paramsRaw"
58
+ ]
59
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.visionhammer.io/la-protocol-v2/scan.uploaded.schema.json",
4
+ "title": "LA protocol v2: scan.uploaded (la->cp)",
5
+ "type": "object",
6
+ "allOf": [
7
+ {
8
+ "$ref": "envelope.schema.json"
9
+ }
10
+ ],
11
+ "properties": {
12
+ "type": {
13
+ "const": "scan.uploaded"
14
+ },
15
+ "payload": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "scanId": {
20
+ "$ref": "defs.schema.json#/$defs/uuid"
21
+ },
22
+ "bytes": {
23
+ "type": "integer",
24
+ "minimum": 0,
25
+ "maximum": 5368709120
26
+ },
27
+ "sha256": {
28
+ "$ref": "defs.schema.json#/$defs/sha256"
29
+ }
30
+ },
31
+ "required": [
32
+ "scanId",
33
+ "bytes",
34
+ "sha256"
35
+ ]
36
+ },
37
+ "seq": {
38
+ "type": "integer",
39
+ "minimum": 1,
40
+ "maximum": 9007199254740991
41
+ },
42
+ "chunk": false,
43
+ "sig": false,
44
+ "paramsRaw": false,
45
+ "agentId": true
46
+ },
47
+ "required": [
48
+ "payload",
49
+ "agentId"
50
+ ]
51
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.visionhammer.io/la-protocol-v2/webhook.forward.schema.json",
4
+ "title": "LA protocol v2: webhook.forward (cp->la)",
5
+ "type": "object",
6
+ "allOf": [
7
+ {
8
+ "$ref": "envelope.schema.json"
9
+ }
10
+ ],
11
+ "properties": {
12
+ "type": {
13
+ "const": "webhook.forward"
14
+ },
15
+ "payload": {
16
+ "type": "object",
17
+ "additionalProperties": false,
18
+ "properties": {
19
+ "webhookId": {
20
+ "$ref": "defs.schema.json#/$defs/uuid"
21
+ },
22
+ "source": {
23
+ "type": "string",
24
+ "enum": [
25
+ "sentry",
26
+ "alertmanager",
27
+ "ios",
28
+ "sonar",
29
+ "snyk",
30
+ "other"
31
+ ]
32
+ },
33
+ "contentType": {
34
+ "type": "string",
35
+ "enum": [
36
+ "application/json",
37
+ "text/plain"
38
+ ]
39
+ },
40
+ "bodyB64": {
41
+ "type": "string",
42
+ "minLength": 0,
43
+ "maxLength": 12288,
44
+ "pattern": "^[A-Za-z0-9+/]*={0,2}$",
45
+ "x-content-class": "transit"
46
+ }
47
+ },
48
+ "required": [
49
+ "webhookId",
50
+ "source",
51
+ "contentType",
52
+ "bodyB64"
53
+ ]
54
+ },
55
+ "seq": {
56
+ "type": "integer",
57
+ "minimum": 1,
58
+ "maximum": 9007199254740991
59
+ },
60
+ "chunk": false,
61
+ "sig": true,
62
+ "paramsRaw": true
63
+ },
64
+ "required": [
65
+ "payload",
66
+ "sig",
67
+ "paramsRaw"
68
+ ]
69
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * contract.js - the CP <-> LA protocol v2 contract (F3-22) as the Local Agent uses it.
3
+ *
4
+ * The JSON Schemas, index.json (allowlist, direction, signed/control flags, limits) and the provenance
5
+ * of the copy are in ./contract (CONTRACT_SOURCE.json: gateway commit + sha256 of every file). Every
6
+ * frame the Local Agent receives AND every frame it sends is validated here with Ajv (draft 2020-12,
7
+ * strict). No schema keyword is interpreted by hand and nothing is added to the contract.
8
+ *
9
+ * validateFrame(frame) -> { ok } | { ok:false, code, keywords[], rules[] }
10
+ * unknown_type `type` is not in the allowlist (index.json)
11
+ * schema_violation the frame does not satisfy the closed schema of its type
12
+ * payload_too_large ... because a string under /payload is above its maxLength (same rule as the gateway)
13
+ * chunk_inconsistent content.response whose byteLength differs from the decoded size of data
14
+ */
15
+ import fs from "node:fs";
16
+ import path from "node:path";
17
+ import { fileURLToPath } from "node:url";
18
+ import Ajv2020 from "ajv/dist/2020.js";
19
+
20
+ const DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), "contract");
21
+ const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
22
+ const readJson = (p) => JSON.parse(fs.readFileSync(p, "utf8"));
23
+
24
+ /** Rules JSON Schema cannot state (README of the contract): byteLength == size of the decoded `data`. */
25
+ export function semanticRules(frame) {
26
+ const rules = [];
27
+ if (frame && frame.type === "content.response" && frame.payload && typeof frame.payload.data === "string") {
28
+ const d = frame.payload.data;
29
+ if (!BASE64.test(d) || Buffer.from(d, "base64").length !== frame.payload.byteLength) rules.push("byteLength-decoded");
30
+ }
31
+ return rules;
32
+ }
33
+
34
+ let singleton = null;
35
+
36
+ /** Loads and compiles the contract once per process (about 20 ms). */
37
+ export function loadContract() {
38
+ if (singleton) return singleton;
39
+ const index = readJson(path.join(DIR, "index.json"));
40
+ const ajv = new Ajv2020({ strict: true, allErrors: true });
41
+ ajv.addKeyword("x-content-class");
42
+ ajv.addKeyword("x-large-metadata");
43
+ for (const f of fs.readdirSync(DIR).filter((n) => n.endsWith(".schema.json"))) ajv.addSchema(readJson(path.join(DIR, f)));
44
+ const validators = new Map();
45
+ for (const [type, info] of Object.entries(index.types)) {
46
+ const v = ajv.getSchema(index.baseId + info.schema);
47
+ if (!v) throw new Error(`contract: no schema for ${type}`);
48
+ validators.set(type, v);
49
+ }
50
+ const defs = readJson(path.join(DIR, "defs.schema.json"));
51
+ const errorCodes = defs.$defs.errorCode.enum;
52
+
53
+ const typeInfo = (type) => (typeof type === "string" && Object.hasOwn(index.types, type) ? index.types[type] : null);
54
+ const accepts = (info, dir) => info.direction === "both" || info.direction === dir;
55
+
56
+ singleton = Object.freeze({
57
+ index,
58
+ limits: Object.freeze({ ...index.limits }),
59
+ errorCodes,
60
+ typeInfo,
61
+ /** Types the Local Agent may receive (cp->la, both). */
62
+ inboundTypes: Object.entries(index.types).filter(([, i]) => accepts(i, "cp->la")).map(([t]) => t),
63
+ /** Types the Local Agent may send (la->cp, both). */
64
+ outboundTypes: Object.entries(index.types).filter(([, i]) => accepts(i, "la->cp")).map(([t]) => t),
65
+ canReceive: (type) => { const i = typeInfo(type); return !!i && accepts(i, "cp->la"); },
66
+ canSend: (type) => { const i = typeInfo(type); return !!i && accepts(i, "la->cp"); },
67
+ validateFrame(frame) {
68
+ if (frame === null || typeof frame !== "object" || typeof frame.type !== "string") {
69
+ // no `type` at all (a v1 command frame, say): the envelope says what is missing
70
+ const envelope = ajv.getSchema(index.baseId + "envelope.schema.json");
71
+ envelope(frame);
72
+ return { ok: false, code: "schema_violation", keywords: (envelope.errors || []).map((e) => e.keyword), rules: [] };
73
+ }
74
+ const info = typeInfo(frame.type);
75
+ if (!info) return { ok: false, code: "unknown_type", keywords: ["enum"], rules: [] };
76
+ const validate = validators.get(frame.type);
77
+ const schemaOk = validate(frame);
78
+ if (!schemaOk) {
79
+ const errs = validate.errors || [];
80
+ const keywords = errs.map((e) => e.keyword);
81
+ const tooLong = errs.some((e) => e.keyword === "maxLength" && String(e.instancePath).startsWith("/payload"));
82
+ return { ok: false, code: tooLong ? "payload_too_large" : "schema_violation", keywords, rules: [] };
83
+ }
84
+ const rules = semanticRules(frame);
85
+ if (rules.length) return { ok: false, code: "chunk_inconsistent", keywords: [], rules };
86
+ return { ok: true };
87
+ },
88
+ });
89
+ return singleton;
90
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * errors.js - wire error codes of protocol v2 (defs.schema.json#/$defs/errorCode).
3
+ *
4
+ * The close code of a fatal error is 4400 + the position of the code in the schema enum (the gateway
5
+ * pins the same rule; `timeout` = 4420). Which codes are fatal / retryable mirrors the gateway's
6
+ * LaV2ErrorCode: a test pins this table against the enum of the copied contract.
7
+ */
8
+ export const ERROR_CODES = Object.freeze([
9
+ "bad_frame", "unknown_type", "schema_violation", "frame_too_large", "payload_too_large", "wrong_direction",
10
+ "handshake_violation", "not_ready", "seq_gap", "seq_replayed", "chunk_out_of_order", "chunk_inconsistent",
11
+ "tenant_mismatch", "agent_mismatch", "params_mismatch", "signature_mode_insufficient", "clock_skew",
12
+ "protocol_unsupported", "unauthorized", "agent_offline", "timeout", "rate_limited", "internal",
13
+ ]);
14
+
15
+ // retryable (the peer may repeat the operation unchanged later)
16
+ const RETRYABLE = new Set(["seq_gap", "agent_offline", "timeout", "rate_limited", "internal"]);
17
+ // NOT fatal: the connection stays up after the error frame
18
+ const NON_FATAL = new Set(["protocol_unsupported", "agent_offline", "timeout", "rate_limited", "internal"]);
19
+
20
+ export const isKnownCode = (code) => ERROR_CODES.includes(code);
21
+ export const isFatal = (code) => isKnownCode(code) && !NON_FATAL.has(code);
22
+ export const isRetryable = (code) => RETRYABLE.has(code);
23
+ export const closeCodeOf = (code) => 4400 + Math.max(0, ERROR_CODES.indexOf(code));
24
+
25
+ /**
26
+ * A refusal with a wire code. `message` never carries frame content: only the code and a fixed reason.
27
+ */
28
+ export class ProtocolError extends Error {
29
+ constructor(code, reason = "") {
30
+ super(reason ? `${code}: ${reason}` : code);
31
+ this.name = "ProtocolError";
32
+ this.code = isKnownCode(code) ? code : "internal";
33
+ }
34
+ }
@@ -0,0 +1,8 @@
1
+ export { PROTOCOL_V2_FLAG, resolveProtocolV2Config, resolveAgentId } from "./config.js";
2
+ export { loadContract, semanticRules } from "./contract.js";
3
+ export { ERROR_CODES, ProtocolError, closeCodeOf, isFatal, isRetryable } from "./errors.js";
4
+ export { parseStrict } from "./strict-json.js";
5
+ export { ChunkStreamTracker, chunkCount } from "./chunks.js";
6
+ export { V2Session, STATE } from "./session.js";
7
+ export { sendContent, sendContentFailure } from "./content.js";
8
+ export { createV2Link, wsTransport, isV2Frame } from "./link.js";
@@ -0,0 +1,93 @@
1
+ /**
2
+ * link.js - glue between the v1 WS tunnel in server.js and the v2 session (F3-05b).
3
+ *
4
+ * The tunnel keeps owning the socket, the reconnect loop, the text `ping` and every v1 command. With
5
+ * LOCAL_AGENT_PROTOCOL_V2=true it asks the link, per connection:
6
+ * begin(ws, legacyRegister) -> true: v2 negotiation started (register + hello); the legacy register is sent
7
+ * later ONLY if the gateway does not speak v2 (no `ready` in handshakeTimeoutMs,
8
+ * or `error protocol_unsupported`); false: nothing to do, send the legacy register now
9
+ * handleMessage(ws, data) -> true when the frame was a v2 frame (consumed); false: it is a v1 command (or noise)
10
+ * and the tunnel runs its existing handler. v1 commands keep working on a v2
11
+ * connection (Edu 2026-10-29: transition; v1 is decommissioned later)
12
+ * onClose(ws) -> the session survives for a resume
13
+ * After a fallback the link stays on v1 for `fallbackMs` (a gateway without v2 would otherwise cost 10 s per reconnect).
14
+ */
15
+ import { V2Session } from "./session.js";
16
+
17
+ /** Adapts a `ws` WebSocket to the transport the session writes to. */
18
+ export function wsTransport(ws) {
19
+ return {
20
+ send: (text) => { if (ws.readyState === 1) ws.send(text); },
21
+ close: (code, reason) => { try { ws.close(code, String(reason).slice(0, 100)); } catch { /* already closed */ } },
22
+ bufferedAmount: () => ws.bufferedAmount || 0,
23
+ };
24
+ }
25
+
26
+ /** v2 frames are the JSON objects that carry `schemaVersion`; a v1 command has `command` and no `schemaVersion`. */
27
+ export function isV2Frame(text, maxFrameBytes) {
28
+ if (text.length > maxFrameBytes) return /"schemaVersion"/.test(text.slice(0, 1024)); // too big to be valid, but still a v2 attempt
29
+ if (text.charCodeAt(0) !== 0x7b) return false;
30
+ try {
31
+ const o = JSON.parse(text);
32
+ return o !== null && typeof o === "object" && !Array.isArray(o) && Object.hasOwn(o, "schemaVersion");
33
+ } catch {
34
+ return false;
35
+ }
36
+ }
37
+
38
+ export function createV2Link({ sessionOptions, log = console, now = Date.now, fallbackMs = 600_000 }) {
39
+ const session = new V2Session({ ...sessionOptions, log: { info: (m) => (log.info || log.log).call(log, m), warn: (m) => log.warn(m) }, now });
40
+ let current = null; // { ws, transport, mode: 'negotiating' | 'v2' | 'v1' }
41
+ let fallbackUntil = 0;
42
+
43
+ const toV1 = (conn, legacyRegister, why) => {
44
+ if (current !== conn || conn.mode === "v1") return;
45
+ conn.mode = "v1";
46
+ fallbackUntil = now() + fallbackMs;
47
+ session.abandon();
48
+ log.warn(`[la-v2] gateway does not speak protocol v2 (${why}); using v1 on this connection`);
49
+ legacyRegister();
50
+ };
51
+
52
+ session.on("ready", () => { if (current && current.mode === "negotiating") current.mode = "v2"; });
53
+
54
+ return {
55
+ session,
56
+ /** @returns {boolean} true when v2 negotiation owns the registration of this connection */
57
+ begin(ws, legacyRegister) {
58
+ if (now() < fallbackUntil) return false;
59
+ const conn = { ws, transport: wsTransport(ws), mode: "negotiating" };
60
+ current = conn;
61
+ session.removeAllListeners("handshake-timeout");
62
+ session.removeAllListeners("unsupported");
63
+ session.removeAllListeners("rejected");
64
+ session.once("handshake-timeout", () => toV1(conn, legacyRegister, "no ready in time"));
65
+ session.once("unsupported", () => toV1(conn, legacyRegister, "protocol_unsupported"));
66
+ session.on("rejected", (code) => log.warn(`[la-v2] gateway refused the v2 handshake: ${code}`));
67
+ try {
68
+ session.attach(conn.transport);
69
+ } catch (e) {
70
+ log.warn(`[la-v2] could not start the handshake (${e.code || "internal"}); using v1`);
71
+ current = null;
72
+ return false;
73
+ }
74
+ return true;
75
+ },
76
+ handleMessage(ws, text, isBinary = false) {
77
+ const conn = current;
78
+ if (!conn || conn.ws !== ws || conn.mode === "v1") return false;
79
+ if (isBinary) { session.handleText("", conn.transport, true); return true; }
80
+ if (!isV2Frame(text, session.limits.maxFrameBytes)) return false;
81
+ session.handleText(text, conn.transport);
82
+ return true;
83
+ },
84
+ onClose(ws) {
85
+ const conn = current;
86
+ if (!conn || conn.ws !== ws) return;
87
+ current = null;
88
+ if (conn.mode !== "v1") session.detach(conn.transport);
89
+ },
90
+ get mode() { return current ? current.mode : null; },
91
+ close() { session.close(); current = null; },
92
+ };
93
+ }