deepdebug-local-agent 1.2.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 (123) hide show
  1. package/.github/workflows/deploy-gcp.yml +58 -26
  2. package/CHANGELOG.md +13 -0
  3. package/docs/la-protocol-v2.md +68 -0
  4. package/docs/ws-metadata-only.md +44 -0
  5. package/env.example +7 -0
  6. package/package.json +2 -1
  7. package/scripts/sync-la-protocol-v2-contract.mjs +58 -0
  8. package/src/la-protocol-v2/chunks.js +45 -0
  9. package/src/la-protocol-v2/config.js +27 -0
  10. package/src/la-protocol-v2/content.js +57 -0
  11. package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +81 -0
  12. package/src/la-protocol-v2/contract/README.md +103 -0
  13. package/src/la-protocol-v2/contract/ack.schema.json +51 -0
  14. package/src/la-protocol-v2/contract/alert.schema.json +62 -0
  15. package/src/la-protocol-v2/contract/approval.decision.schema.json +57 -0
  16. package/src/la-protocol-v2/contract/audit.event.schema.json +86 -0
  17. package/src/la-protocol-v2/contract/config.update.schema.json +73 -0
  18. package/src/la-protocol-v2/contract/content.request.schema.json +110 -0
  19. package/src/la-protocol-v2/contract/content.response.schema.json +90 -0
  20. package/src/la-protocol-v2/contract/defs.schema.json +191 -0
  21. package/src/la-protocol-v2/contract/envelope.schema.json +89 -0
  22. package/src/la-protocol-v2/contract/error.schema.json +59 -0
  23. package/src/la-protocol-v2/contract/heartbeat.schema.json +100 -0
  24. package/src/la-protocol-v2/contract/hello.schema.json +111 -0
  25. package/src/la-protocol-v2/contract/incident.candidate.schema.json +102 -0
  26. package/src/la-protocol-v2/contract/index.json +166 -0
  27. package/src/la-protocol-v2/contract/job.assign.schema.json +212 -0
  28. package/src/la-protocol-v2/contract/job.cancel.schema.json +52 -0
  29. package/src/la-protocol-v2/contract/job.event.schema.json +115 -0
  30. package/src/la-protocol-v2/contract/job.result.schema.json +100 -0
  31. package/src/la-protocol-v2/contract/log.stats.schema.json +79 -0
  32. package/src/la-protocol-v2/contract/ready.schema.json +106 -0
  33. package/src/la-protocol-v2/contract/register.schema.json +92 -0
  34. package/src/la-protocol-v2/contract/scan.upload.schema.json +59 -0
  35. package/src/la-protocol-v2/contract/scan.uploaded.schema.json +51 -0
  36. package/src/la-protocol-v2/contract/webhook.forward.schema.json +69 -0
  37. package/src/la-protocol-v2/contract.js +90 -0
  38. package/src/la-protocol-v2/errors.js +34 -0
  39. package/src/la-protocol-v2/index.js +8 -0
  40. package/src/la-protocol-v2/link.js +93 -0
  41. package/src/la-protocol-v2/runtime.js +72 -0
  42. package/src/la-protocol-v2/session.js +650 -0
  43. package/src/la-protocol-v2/strict-json.js +47 -0
  44. package/src/mcp-http-server.js +5 -3
  45. package/src/server.js +70 -9
  46. package/src/utils/exec-policy.js +8 -1
  47. package/src/ws-channel/allowlist.js +143 -0
  48. package/src/ws-channel/config.js +22 -0
  49. package/src/ws-channel/connection-state.js +18 -0
  50. package/src/ws-channel/index.js +6 -0
  51. package/src/ws-channel/metadata-guard.js +78 -0
  52. package/src/ws-channel/policy-offline.js +40 -0
  53. package/src/ws-channel/transit-guard.js +55 -0
  54. package/test/contract/la-protocol-v2/vectors/ack.vectors.json +149 -0
  55. package/test/contract/la-protocol-v2/vectors/alert.vectors.json +101 -0
  56. package/test/contract/la-protocol-v2/vectors/approval.decision.vectors.json +165 -0
  57. package/test/contract/la-protocol-v2/vectors/audit.event.vectors.json +156 -0
  58. package/test/contract/la-protocol-v2/vectors/config.update.vectors.json +189 -0
  59. package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +325 -0
  60. package/test/contract/la-protocol-v2/vectors/content.response.vectors.json +413 -0
  61. package/test/contract/la-protocol-v2/vectors/cross-type.vectors.json +260 -0
  62. package/test/contract/la-protocol-v2/vectors/error.vectors.json +179 -0
  63. package/test/contract/la-protocol-v2/vectors/heartbeat.vectors.json +109 -0
  64. package/test/contract/la-protocol-v2/vectors/hello.vectors.json +224 -0
  65. package/test/contract/la-protocol-v2/vectors/incident.candidate.vectors.json +186 -0
  66. package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +631 -0
  67. package/test/contract/la-protocol-v2/vectors/job.cancel.vectors.json +133 -0
  68. package/test/contract/la-protocol-v2/vectors/job.event.vectors.json +233 -0
  69. package/test/contract/la-protocol-v2/vectors/job.result.vectors.json +137 -0
  70. package/test/contract/la-protocol-v2/vectors/log.stats.vectors.json +137 -0
  71. package/test/contract/la-protocol-v2/vectors/ready.vectors.json +177 -0
  72. package/test/contract/la-protocol-v2/vectors/register.vectors.json +399 -0
  73. package/test/contract/la-protocol-v2/vectors/scan.upload.vectors.json +137 -0
  74. package/test/contract/la-protocol-v2/vectors/scan.uploaded.vectors.json +101 -0
  75. package/test/contract/la-protocol-v2/vectors/traces/invalid-agent-mismatch.json +140 -0
  76. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-after-last.json +200 -0
  77. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-byte-length-mismatch.json +174 -0
  78. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-last-flag.json +200 -0
  79. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-out-of-order.json +200 -0
  80. package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-total-changes.json +200 -0
  81. package/test/contract/la-protocol-v2/vectors/traces/invalid-clock-skew.json +46 -0
  82. package/test/contract/la-protocol-v2/vectors/traces/invalid-event-before-ready.json +72 -0
  83. package/test/contract/la-protocol-v2/vectors/traces/invalid-first-frame-not-register.json +48 -0
  84. package/test/contract/la-protocol-v2/vectors/traces/invalid-frame-over-48kib.json +141 -0
  85. package/test/contract/la-protocol-v2/vectors/traces/invalid-la-sends-job-assign.json +173 -0
  86. package/test/contract/la-protocol-v2/vectors/traces/invalid-params-raw-differs-from-payload.json +172 -0
  87. package/test/contract/la-protocol-v2/vectors/traces/invalid-payload-above-limit.json +174 -0
  88. package/test/contract/la-protocol-v2/vectors/traces/invalid-resume-restarts-seq.json +144 -0
  89. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-gap.json +166 -0
  90. package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-repeated.json +166 -0
  91. package/test/contract/la-protocol-v2/vectors/traces/invalid-sig-tenant-mismatch.json +172 -0
  92. package/test/contract/la-protocol-v2/vectors/traces/invalid-signature-warn-with-enforce-required.json +77 -0
  93. package/test/contract/la-protocol-v2/vectors/traces/invalid-tenant-mismatch.json +140 -0
  94. package/test/contract/la-protocol-v2/vectors/traces/invalid-type-outside-allowlist.json +140 -0
  95. package/test/contract/la-protocol-v2/vectors/traces/valid-content-chunks-in-order.json +224 -0
  96. package/test/contract/la-protocol-v2/vectors/traces/valid-handshake-and-job.json +266 -0
  97. package/test/contract/la-protocol-v2/vectors/traces/valid-resume-continues-seq.json +168 -0
  98. package/test/contract/la-protocol-v2/vectors/traces/valid-retransmit-is-idempotent.json +193 -0
  99. package/test/contract/la-protocol-v2/vectors/webhook.forward.vectors.json +170 -0
  100. package/test/la-protocol-v2/contract.test.js +131 -0
  101. package/test/la-protocol-v2/helpers/la-rig.js +83 -0
  102. package/test/la-protocol-v2/helpers/reference-cp.js +349 -0
  103. package/test/la-protocol-v2/helpers/spawn-agent.js +35 -0
  104. package/test/la-protocol-v2/link.test.js +141 -0
  105. package/test/la-protocol-v2/session-inbound.test.js +279 -0
  106. package/test/la-protocol-v2/session-outbound.test.js +382 -0
  107. package/test/la-protocol-v2/traces.test.js +270 -0
  108. package/test/la-protocol-v2/tunnel-e2e.test.js +124 -0
  109. package/test/ws-channel/allowlist.test.js +67 -0
  110. package/test/ws-channel/metadata-guard.test.js +104 -0
  111. package/test/ws-channel/policy-offline.test.js +99 -0
  112. package/test/ws-channel/tunnel-e2e.test.js +88 -0
  113. package/tools/graphify-validation/extras/README.md +19 -0
  114. package/tools/graphify-validation/extras/bymethod.py +35 -0
  115. package/tools/graphify-validation/extras/docker/Dockerfile +16 -0
  116. package/tools/graphify-validation/extras/docker/inner.sh +7 -0
  117. package/tools/graphify-validation/extras/g10.py +59 -0
  118. package/tools/graphify-validation/extras/g3aug.py +49 -0
  119. package/tools/graphify-validation/extras/hybrid.py +34 -0
  120. package/tools/graphify-validation/extras/truth2.py +44 -0
  121. package/tools/interop/LaV2NodeInteropTest.java +266 -0
  122. package/tools/interop/README.md +21 -0
  123. package/tools/interop/la-interop-client.mjs +97 -0
@@ -1,4 +1,8 @@
1
- # Deploy local-agent (Node) to GCP Cloud Run (visionhammer) via WIF.
1
+ # Deploy local-agent (Node) to GCP Cloud Run via WIF.
2
+ # QA: project visionhammer (europe-west1 since Plano QA Alvo EU 2026-10-29, same
3
+ # conventions as PROD: deploy-local-agent@ SA of the nonprod project, EU registry,
4
+ # immutable sha-<commit> tag, digest-pinned image-only deploy). PROD: project
5
+ # visionhammer-prod (europe-west1), own WIF pool/deploy SA, digest-pinned deploy.
2
6
  # Manual-only: this repo has no develop branch and several active feature
3
7
  # branches, so deploys are triggered on demand from the Actions tab. Choose the
4
8
  # environment input. Replaces the insptechai Cloud Build path.
@@ -23,14 +27,18 @@ permissions:
23
27
  id-token: write
24
28
 
25
29
  env:
26
- GCP_PROJECT: visionhammer
27
- # EU migration (PLANO-MIGRACAO-EU-PROD.md): PROD moves to europe-west1. The
28
- # region comes from repo variables (PROD_REGION / QA_REGION); today's value
29
- # stays the default, so nothing changes until the variable is set.
30
- GCP_REGION: ${{ inputs.environment == 'prod' && (vars.PROD_REGION || 'us-central1') || (vars.QA_REGION || 'us-central1') }}
31
- WIF_PROVIDER: projects/1009366917257/locations/global/workloadIdentityPools/github-pool/providers/github-provider
32
- DEPLOYER_SA: github-deployer@visionhammer.iam.gserviceaccount.com
33
- AR_IMAGE: us-central1-docker.pkg.dev/visionhammer/visionhammer-docker/local-agent
30
+ # PROD lives in visionhammer-prod / europe-west1 (hardcoded: no repo variable
31
+ # can silently repoint PROD). QA lives in visionhammer / europe-west1 (QA_REGION
32
+ # only as an escape hatch) with its own deploy SA and the EU registry. The QA
33
+ # WIF provider is a repo variable so the pool/provider adopted by vh-infra
34
+ # (live/nonprod/_project) can be flipped without a commit:
35
+ # gh variable set QA_WIF_PROVIDER --body projects/1009366917257/locations/global/workloadIdentityPools/<pool>/providers/<provider>
36
+ GCP_PROJECT: ${{ inputs.environment == 'prod' && 'visionhammer-prod' || 'visionhammer' }}
37
+ GCP_REGION: ${{ inputs.environment == 'prod' && 'europe-west1' || (vars.QA_REGION || 'europe-west1') }}
38
+ WIF_PROVIDER: ${{ inputs.environment == 'prod' && 'projects/616585643354/locations/global/workloadIdentityPools/github/providers/github-actions' || (vars.QA_WIF_PROVIDER || 'projects/1009366917257/locations/global/workloadIdentityPools/vh-infra/providers/github-actions') }}
39
+ DEPLOYER_SA: ${{ inputs.environment == 'prod' && 'deploy-local-agent@visionhammer-prod.iam.gserviceaccount.com' || 'deploy-local-agent@visionhammer.iam.gserviceaccount.com' }}
40
+ AR_HOST: europe-west1-docker.pkg.dev
41
+ AR_IMAGE: ${{ inputs.environment == 'prod' && 'europe-west1-docker.pkg.dev/visionhammer-prod/vh-docker/local-agent' || 'europe-west1-docker.pkg.dev/visionhammer/visionhammer-docker-eu/local-agent' }}
34
42
 
35
43
  jobs:
36
44
  deploy:
@@ -51,10 +59,12 @@ jobs:
51
59
  fi
52
60
  echo "service=deepdebug-local-agent" >> "$GITHUB_OUTPUT"
53
61
  echo "tag=prod-${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT"
62
+ echo "image_tag=sha-${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
54
63
  echo "traffic=canary" >> "$GITHUB_OUTPUT"
55
64
  else
56
65
  echo "service=deepdebug-local-agent-qa" >> "$GITHUB_OUTPUT"
57
66
  echo "tag=qa-${GITHUB_SHA::8}" >> "$GITHUB_OUTPUT"
67
+ echo "image_tag=sha-${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
58
68
  echo "traffic=live" >> "$GITHUB_OUTPUT"
59
69
  fi
60
70
 
@@ -68,35 +78,57 @@ jobs:
68
78
  uses: google-github-actions/setup-gcloud@v2
69
79
 
70
80
  - name: Configure Docker for Artifact Registry
71
- run: gcloud auth configure-docker us-central1-docker.pkg.dev --quiet
81
+ run: gcloud auth configure-docker "$AR_HOST" --quiet
72
82
 
73
- - name: Build & push image (multi-stage Node Dockerfile)
83
+ # QA and PROD: immutable tag sha-<full SHA> (a re-run of the same SHA fails
84
+ # the push by design); the deploy references the pushed DIGEST, not the tag.
85
+ - name: Build & push image (immutable sha tag, resolve digest)
74
86
  run: |
75
- IMG="${AR_IMAGE}:${{ steps.vars.outputs.tag }}"
76
- docker build --platform=linux/amd64 -t "$IMG" .
77
- docker push "$IMG"
78
- echo "IMG=$IMG" >> "$GITHUB_ENV"
87
+ TAGGED="${AR_IMAGE}:${{ steps.vars.outputs.image_tag }}"
88
+ docker build --platform=linux/amd64 -t "$TAGGED" .
89
+ docker push "$TAGGED"
90
+ DIGEST="$(docker inspect --format='{{index .RepoDigests 0}}' "$TAGGED")"
91
+ case "$DIGEST" in
92
+ "${AR_IMAGE}@sha256:"*) ;;
93
+ *) echo "::error::Unexpected digest reference: $DIGEST"; exit 1 ;;
94
+ esac
95
+ echo "IMG=$DIGEST" >> "$GITHUB_ENV"
79
96
 
80
- - name: Deploy to Cloud Run (image only; preserves env/secrets)
97
+ - name: Deploy to Cloud Run (QA, image only by digest, live)
98
+ if: inputs.environment != 'prod'
81
99
  run: |
82
- # PROD lands at 0% traffic behind a tag (canary); verify the tagged
83
- # URL, then promote with `gcloud run services update-traffic`.
84
- # QA keeps deploying live. Mirrors vh-api-gateway deploy-gcp.yml.
100
+ # QA (europe-west1) keeps deploying live. The service is Terraform-owned
101
+ # (vh-infra live/nonprod/qa/20-services: env, secrets, Filestore mount,
102
+ # min=max=1), so this is an image-only `services update` on the `app`
103
+ # container: never --set-env-vars / --update-env-vars / scaling flags.
104
+ # Service-level flags BEFORE --container.
85
105
  #
86
106
  # --vpc-egress is pinned: gcloud 572 flipped the template to
87
107
  # all-traffic on an image-only update; with no Cloud NAT that cuts
88
108
  # the service's internet egress.
89
- EXTRA=""
90
- if [ "${{ steps.vars.outputs.traffic }}" = "canary" ]; then
91
- EXTRA="--no-traffic --tag ${{ steps.vars.outputs.tag }}"
92
- fi
93
- gcloud run deploy "${{ steps.vars.outputs.service }}" \
109
+ gcloud run services update "${{ steps.vars.outputs.service }}" \
110
+ --project "$GCP_PROJECT" \
111
+ --region "$GCP_REGION" \
112
+ --vpc-egress=private-ranges-only \
113
+ --quiet \
114
+ --container=app \
115
+ --image="$IMG"
116
+
117
+ # PROD: `services update` with image only (Terraform owns env/secrets, so
118
+ # no --set-env-vars). Singleton service (min=max=1): the revision lands at
119
+ # 0% behind a tag; promote with `gcloud run services update-traffic`
120
+ # (--to-revisions) once the tagged revision is verified.
121
+ - name: Deploy to Cloud Run (PROD, digest, canary)
122
+ if: inputs.environment == 'prod'
123
+ run: |
124
+ gcloud run services update "${{ steps.vars.outputs.service }}" \
94
125
  --image "$IMG" \
95
126
  --project "$GCP_PROJECT" \
96
127
  --region "$GCP_REGION" \
97
128
  --vpc-egress=private-ranges-only \
98
- $EXTRA \
129
+ --no-traffic --tag "${{ steps.vars.outputs.tag }}" \
99
130
  --quiet
100
131
 
101
132
  - name: Summary
102
- run: echo "Deployed **${{ steps.vars.outputs.service }}** with \`$IMG\`" >> "$GITHUB_STEP_SUMMARY"
133
+ run: |
134
+ echo "Deployed **${{ steps.vars.outputs.service }}** (${GCP_PROJECT}/${GCP_REGION}) with \`$IMG\`" >> "$GITHUB_STEP_SUMMARY"
package/CHANGELOG.md CHANGED
@@ -6,6 +6,19 @@ Security release (security review 2026-10-11, C1 and H4). The agent now fails
6
6
  closed by default. Read "Upgrade" before rolling it out to agents whose
7
7
  platform side does not sign commands yet.
8
8
 
9
+ ### Added (opt-in, off by default)
10
+
11
+ - **`LOCAL_AGENT_PROTOCOL_V2=true`** (F3-05b, default off). The Local Agent side of the CP <-> LA protocol v2:
12
+ register/hello/ready with resume, seq/ack/retransmission, chunking of `content.response`, allowlist, every
13
+ frame validated against the gateway's JSON Schemas, Ed25519 check of signed CP frames, `signatureMode` in
14
+ `hello`. Falls back to the v1 register on the same connection when the gateway does not speak v2; v1 commands
15
+ keep working. Transport only: no content handlers yet. See `docs/la-protocol-v2.md`. No behaviour change when off.
16
+
17
+ - **`LOCAL_AGENT_WS_METADATA_ONLY=off|enforce`** (T4-10, D17, D19). `enforce`
18
+ restricts the outbound WS channel to allowlisted metadata (ids, hashes,
19
+ counts, state, cost), refuses legacy content commands and denies
20
+ PolicyGate-backed actions while the control plane is unreachable. See
21
+ `docs/ws-metadata-only.md`. No behaviour change when off.
9
22
  ### Added
10
23
 
11
24
  - **DLP module (`src/dlp/`, flag `LOCAL_AGENT_DLP_MODE=off|warn|enforce`, default `off`).**
@@ -0,0 +1,68 @@
1
+ # Protocol v2 on the Local Agent (F3-05b)
2
+
3
+ The Local Agent side of the CP <-> LA WebSocket protocol v2. The contract is owned by the gateway
4
+ (`docs/contracts/la-protocol-v2` of vh-api-gateway, F3-22); this repo keeps a verified copy in
5
+ `src/la-protocol-v2/contract/` (`CONTRACT_SOURCE.json` records the gateway commit and the sha256 of every
6
+ file; `npm test` fails if a copy was edited). Update it with `node scripts/sync-la-protocol-v2-contract.mjs
7
+ <gateway contract dir> --commit <sha>`; never edit the copy.
8
+
9
+ ## Flag
10
+
11
+ `LOCAL_AGENT_PROTOCOL_V2=true` (default **off**; unset, empty or unrecognised = off). With it off the tunnel in
12
+ `src/server.js` is the v1 client, byte for byte: no v2 module (nor Ajv, nor the schemas) is loaded.
13
+
14
+ It also needs `TENANT_ID` to be a UUID and the agent id (`authorizationId` of `~/.deepdebug/config.json`, or
15
+ `LOCAL_AGENT_ID`); without them v2 stays off with a warning. Tuning (clamped): `LOCAL_AGENT_V2_MAX_IN_FLIGHT`
16
+ (frames unacked, default 8), `LOCAL_AGENT_V2_HANDSHAKE_MS` (wait for `ready`, default 10000).
17
+
18
+ ## What it does
19
+
20
+ - **Handshake.** On each socket: `register` (`protocol:2`, version, capabilities, `signatureMode`, workspaces as
21
+ `{workspaceId, pathSha256}`, `resume{sessionId,lastRecvSeq}` when it has a session) then `hello`. The gateway
22
+ answers a signed `ready` (`sessionId`, `resumed`, `peerLastSeq`, limits). Paths never leave the machine, only hashes.
23
+ - **Fallback to v1** on the same connection: no `ready` in 10 s, or `error protocol_unsupported`. The v1
24
+ register is then sent, and v2 is not offered again for 10 min. A refusal that v1 would not fix
25
+ (`signature_mode_insufficient`, `clock_skew`, ...) does not fall back.
26
+ - **Signatures.** Every signed CP->LA frame (`ready` included) is checked like a v1 command: `paramsRaw` must equal
27
+ `payload` (duplicate keys refused), `sig` must be bound to the frame's tenant and workspace, and the Ed25519
28
+ signature must verify with the trusted keys (`VH-AGENT-SIG-V1`, method `WS`, path = type). `enforce` refuses,
29
+ `warn` logs and goes on, `off` does not verify. `hello.signatureMode` reports the real mode; the gateway
30
+ refuses `hybrid`/`client` tenants that are not in `enforce`.
31
+ - **Validation of everything.** Each received and each sent frame is validated with Ajv (2020-12, strict) against
32
+ the closed schemas, after the allowlist (21 types; direction checked) and the size limit (48 KiB). A refusal is an
33
+ `error` frame with the wire code only (never an echo of the content) and, for fatal codes, a WS close
34
+ `4400 + position of the code` (`timeout` = 4420).
35
+ - **Numbering and acks.** `seq` per direction from 1, `ack` piggybacked, an `ack` frame per accepted data frame
36
+ (`duplicate` for a retransmission, never executed twice: the last 512 `seq`->`msgId` pairs are kept).
37
+ A frame with no handler is acked `rejected protocol_unsupported`.
38
+ - **Sending.** At most N frames unacked (the rest wait in a bounded queue, 256), retransmitted after
39
+ `ackTimeoutMs` (same bytes: same seq and msgId), at most 3 times, then `timeout` to the caller and the socket
40
+ is closed 4420 (the frame is kept for the resume). A `heartbeat` frame every `heartbeatSeconds`; the text `ping`
41
+ of v1 stays as liveness.
42
+ - **Resume.** The session survives a dropped socket for 120 s (the gateway keeps it 90 s, same instance only).
43
+ `resumed:false` restarts both numberings at 1 and fails the frames that were in flight (`agent_offline`).
44
+ - **Chunking** (`src/la-protocol-v2/content.js`): `sendContent(session, {requestId, source})` splits a `Buffer` (or
45
+ `{size, read}`, read lazily) into `content.response` chunks of at most 32 KiB raw (43692 base64 chars, frame
46
+ under 48 KiB), at most 64 per response (2 MiB), `chunk{index,total,last}`, `byteLength` = decoded size. The
47
+ chunk rules of the contract are enforced on what is sent; a stream cannot continue across a reset.
48
+ - **v1 commands on a v2 connection** (decision of 2026-10-29: transition, v1 is decommissioned later). A frame
49
+ without `schemaVersion` is not a v2 frame: it goes to the existing v1 handler (`handleWsCommand`), which
50
+ answers in the v1 shape. **The gateway (F3-05a) still refuses to send v1 commands to a v2 agent
51
+ (`protocol_mismatch`)**: this is ready on the LA side, but it will only be exercised once the gateway sends them.
52
+
53
+ ## What it does NOT do
54
+
55
+ No handler of `content.request` (F3-07), no DLP on what is sent (F3-09, must run before `sendContent` is
56
+ called), no log/incident detector (F3-11b), no `job.assign` runtime (F3-08). Those frames are acked
57
+ `rejected protocol_unsupported`. `sha256` of `content.response` is not sent (the contract does not say what it covers).
58
+
59
+ ## Tests
60
+
61
+ `npm test` (node:test), all in `test/la-protocol-v2/`:
62
+ `contract.test.js` (all 134 vectors of F3-22, valid and invalid, plus the drift check), `traces.test.js` (the 24
63
+ traces), `session-inbound.test.js`, `session-outbound.test.js`, `link.test.js`, `tunnel-e2e.test.js` (the real
64
+ `src/server.js` against a reference CP; flag off = v1 frames). The reference CP (`test/la-protocol-v2/helpers/reference-cp.js`)
65
+ is a Node model of the gateway rules, not the gateway.
66
+
67
+ **Interoperability with the real gateway session** (`LaV2Session`, Java, real Reactor Netty socket on loopback, real
68
+ Ed25519 signer): `tools/interop/` (`README.md`). Not part of `npm test` (needs a gateway checkout, JDK 21, Maven).
@@ -0,0 +1,44 @@
1
+ # WS metadata-only mode (T4-10, D17, D19)
2
+
3
+ Flag: `LOCAL_AGENT_WS_METADATA_ONLY=off|enforce` (default `off`; an unrecognised value fails closed to `enforce`).
4
+
5
+ ## What `enforce` does
6
+
7
+ 1. **Outbound allowlist** (`src/ws-channel/allowlist.js`). Only these message types leave the agent:
8
+ `register`, `hello`, `status`, `cost` and command replies (`requestId`/`command`/`ok`/`status`/`openWorkspaces`/`errorCode`).
9
+ Fields are typed as ids, sha256 hashes, bounded integers, booleans or fixed enums. There is no free-text
10
+ field, so code, diffs, logs and chat text cannot be carried. `register` sends `workspaceHash` (sha256) instead of `workspacePath`.
11
+ 2. **Legacy content commands are refused before execution** (`workspace.file-content`, `workspace.diff`,
12
+ `workspace.safe-patch`, `mcp.*`, ...). Only `health` and `workspace.open` run; the reply is
13
+ `{ok:false, errorCode:"blocked_metadata_only"}`. Free-text error messages become `errorCode`.
14
+ 3. **Rejections are logged without content**: `[ws-metadata] BLOCKED {"reason":..,"type":..,"field":..}` (field names and codes only).
15
+ 4. **D17 transit controls**: nothing is persisted or queued by the module (blocked/over-limit messages are dropped);
16
+ a reply is sent only once, for a known request, within 60 s (TTL); 4 KiB per message; 200 messages per 10 s; 256 in-flight requests.
17
+ 5. **D19 offline = DENY**: while the tunnel is not open (or silent for more than 90 s), exec decisions that depend on
18
+ PolicyGate (signed platform tool commands, human-approval tokens) are denied with `via=policy-gate-offline`.
19
+ Allowlisted local commands do not need PolicyGate and are unaffected. Note: with `DISABLE_WS_TUNNEL=true` the agent is
20
+ permanently "offline", so these actions are always denied under `enforce` (a startup warning is logged; product decision for standalone agents pending, see Open points).
21
+ The same guard is shared with the MCP bridge (`/mcp/execute-command`), so the DENY applies to both exec entry points.
22
+
23
+ ## Reserved contract and known limits
24
+
25
+ - `hello`, `status` and `cost` are **reserved contract types: nothing emits them yet**. They are in the allowlist so that the
26
+ protocol v2 emitters (F3-05b, not merged) can be added without widening the closed world. Only `register` and command replies are sent today.
27
+ - `workspace.open` stays allowed in `enforce` (D16: VH is authoritative). It can still change local state (it sets the workspace root
28
+ from `params.root` sent by the control plane, protected by request signing); only its reply is projected to metadata.
29
+ - D19 liveness relies on the gateway answering the protocol `ping` (or the text `ping` with `pong`) within 90 s. This has **not** been
30
+ verified against the real QA gateway; validate it before enabling `enforce` in QA (open point on the PR).
31
+
32
+ ## What does not change
33
+
34
+ With the flag off (default) the tunnel, the register frame, command handling and exec policy are byte-for-byte the legacy ones.
35
+ The module opens no socket of its own. DLP (`src/dlp/*`), signing, bind and Helm are untouched.
36
+
37
+ ## Rollback
38
+
39
+ Unset `LOCAL_AGENT_WS_METADATA_ONLY` (or set `off`) and restart the agent.
40
+
41
+ ## Extending the contract
42
+
43
+ Add the type/field to `OUTBOUND_SCHEMAS` and a golden example to `test/ws-channel/allowlist.test.js` (the closed-set test fails on purpose).
44
+ The CP-side contract (F3-05b protocol v2) is not merged yet; align the schema with it when it lands.
package/env.example CHANGED
@@ -70,6 +70,13 @@ LOG_LEVEL=info
70
70
  # LOCAL_AGENT_WORKSPACE_ID=
71
71
  # LOCAL_AGENT_REQUIRE_WORKSPACE=false
72
72
 
73
+ # LOCAL_AGENT_WS_METADATA_ONLY=off|enforce (default off). enforce: the outbound WS channel to the
74
+ # control plane carries ONLY allowlisted metadata (ids, hashes, counts, state, cost) and never
75
+ # content (code, diffs, logs, chat). Legacy content commands are refused in this mode, and
76
+ # PolicyGate-backed actions (platform tool commands, human approvals) are DENIED while the agent
77
+ # has no live connection to the control plane (D19). Unrecognised values fail closed to enforce.
78
+ # LOCAL_AGENT_WS_METADATA_ONLY=off
79
+
73
80
  # DLP (T4-09): mask secrets/PII before content leaves the agent. Default off.
74
81
  # LOCAL_AGENT_DLP_MODE=off|warn|enforce (warn = count only, enforce = redact)
75
82
  # LOCAL_AGENT_DLP_BLOCK_CLASSES=private-key,cloud-credential (enforce: withhold whole text)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepdebug-local-agent",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "description": "DeepDebug Local Agent - AI-powered code debugging assistant",
5
5
  "private": false,
6
6
  "type": "module",
@@ -34,6 +34,7 @@
34
34
  "dependencies": {
35
35
  "@anthropic-ai/sdk": "^0.39.0",
36
36
  "@modelcontextprotocol/sdk": "^1.0.4",
37
+ "ajv": "^8.20.0",
37
38
  "body-parser": "^1.20.3",
38
39
  "chokidar": "^3.6.0",
39
40
  "cors": "^2.8.5",
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ // scripts/sync-la-protocol-v2-contract.mjs - copies the CP <-> LA protocol v2 contract (F3-22) from a
3
+ // vh-api-gateway checkout into this repo and records where it came from.
4
+ //
5
+ // node scripts/sync-la-protocol-v2-contract.mjs <gateway-contract-dir> --commit <sha> [--tip <sha>]
6
+ //
7
+ // <gateway-contract-dir> is docs/contracts/la-protocol-v2 of vh-api-gateway (a worktree of release/qa, or
8
+ // the output of `git archive`). The gateway folder stays the only source of truth: never edit the copies.
9
+ // src/la-protocol-v2/contract/ *.schema.json + index.json (shipped: the runtime validates against them)
10
+ // test/contract/la-protocol-v2/ vectors/ (valid + invalid messages, traces) used by the tests
11
+ // src/la-protocol-v2/contract/CONTRACT_SOURCE.json commit + sha256 of every copied file (a test fails on drift)
12
+ import crypto from "node:crypto";
13
+ import fs from "node:fs";
14
+ import path from "node:path";
15
+ import { fileURLToPath } from "node:url";
16
+
17
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
18
+ const args = process.argv.slice(2);
19
+ const src = args.find((a) => !a.startsWith("--"));
20
+ const opt = (name) => { const i = args.indexOf(`--${name}`); return i >= 0 ? args[i + 1] : undefined; };
21
+ const commit = opt("commit");
22
+ if (!src || !commit || !/^[0-9a-f]{40}$/.test(commit)) {
23
+ console.error("usage: sync-la-protocol-v2-contract.mjs <gateway docs/contracts/la-protocol-v2> --commit <40-hex sha> [--tip <sha>]");
24
+ process.exit(2);
25
+ }
26
+ const sha256 = (buf) => crypto.createHash("sha256").update(buf).digest("hex");
27
+ const runtimeDir = path.join(ROOT, "src/la-protocol-v2/contract");
28
+ const vectorsDir = path.join(ROOT, "test/contract/la-protocol-v2");
29
+ fs.rmSync(vectorsDir, { recursive: true, force: true });
30
+ fs.mkdirSync(runtimeDir, { recursive: true });
31
+ fs.mkdirSync(vectorsDir, { recursive: true });
32
+
33
+ const files = {};
34
+ for (const name of fs.readdirSync(src).filter((n) => n.endsWith(".schema.json") || n === "index.json" || n === "README.md")) {
35
+ const buf = fs.readFileSync(path.join(src, name));
36
+ fs.writeFileSync(path.join(runtimeDir, name), buf);
37
+ files[name] = sha256(buf);
38
+ }
39
+ function copyTree(from, to, rel) {
40
+ fs.mkdirSync(to, { recursive: true });
41
+ for (const e of fs.readdirSync(from, { withFileTypes: true })) {
42
+ if (e.isDirectory()) { copyTree(path.join(from, e.name), path.join(to, e.name), `${rel}/${e.name}`); continue; }
43
+ const buf = fs.readFileSync(path.join(from, e.name));
44
+ fs.writeFileSync(path.join(to, e.name), buf);
45
+ files[`${rel}/${e.name}`] = sha256(buf);
46
+ }
47
+ }
48
+ copyTree(path.join(src, "vectors"), path.join(vectorsDir, "vectors"), "vectors");
49
+ fs.writeFileSync(path.join(runtimeDir, "CONTRACT_SOURCE.json"), JSON.stringify({
50
+ repo: "VISIONHAMMER/vh-api-gateway",
51
+ path: "docs/contracts/la-protocol-v2",
52
+ branch: "release/qa",
53
+ commit,
54
+ ...(opt("tip") ? { branchTip: opt("tip") } : {}),
55
+ note: "Copied by scripts/sync-la-protocol-v2-contract.mjs. Do not edit: change the gateway folder and sync again.",
56
+ files,
57
+ }, null, 2) + "\n");
58
+ console.log(`synced ${Object.keys(files).length} files from ${commit.slice(0, 8)}`);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * chunks.js - the chunk rules of protocol v2 (README of the contract: "chunks") for ONE direction.
3
+ *
4
+ * A `content.response` is a stream of frames with the same requestId: chunk.index 0.., chunk.total constant,
5
+ * chunk.last true only on the last index, nothing after last, every payload.data at most chunkRawBytes
6
+ * decoded. The Local Agent applies them to what it SENDS (so it can never emit a stream the gateway would
7
+ * refuse with a fatal error); the same class is how the tests check the traces.
8
+ */
9
+ const MAX_STREAMS = 256;
10
+
11
+ /** How many chunks a payload of `size` bytes needs. An empty payload is still one (empty) chunk. */
12
+ export function chunkCount(size, chunkRawBytes) {
13
+ return Math.max(1, Math.ceil(size / chunkRawBytes));
14
+ }
15
+
16
+ export class ChunkStreamTracker {
17
+ constructor({ chunkRawBytes = 32768, maxChunks = 64 } = {}) {
18
+ this.chunkRawBytes = chunkRawBytes;
19
+ this.maxChunks = maxChunks;
20
+ this.streams = new Map(); // requestId -> { total, next, done }
21
+ }
22
+
23
+ /**
24
+ * Checks one chunk against the stream state and, when it is fine, records it.
25
+ * @returns {null | "chunk_out_of_order" | "chunk_inconsistent"}
26
+ */
27
+ accept(requestId, chunk, decodedBytes) {
28
+ const { index, total, last } = chunk;
29
+ const st = this.streams.get(requestId);
30
+ if (st && st.done) return "chunk_out_of_order";
31
+ if (st && st.total !== total) return "chunk_inconsistent";
32
+ if (total > this.maxChunks) return "chunk_inconsistent";
33
+ const expected = st ? st.next : 0;
34
+ if (index !== expected) return "chunk_out_of_order";
35
+ if (last !== (index === total - 1)) return "chunk_inconsistent";
36
+ if (decodedBytes > this.chunkRawBytes) return "chunk_inconsistent";
37
+ this.streams.delete(requestId);
38
+ this.streams.set(requestId, { total, next: index + 1, done: last });
39
+ while (this.streams.size > MAX_STREAMS) this.streams.delete(this.streams.keys().next().value);
40
+ return null;
41
+ }
42
+
43
+ forget(requestId) { this.streams.delete(requestId); }
44
+ clear() { this.streams.clear(); }
45
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * config.js - LOCAL_AGENT_PROTOCOL_V2 flag (F3-05b).
3
+ *
4
+ * false (default) the WS tunnel is the v1 client, byte for byte: no v2 module is loaded.
5
+ * true the tunnel offers protocol v2 first (register/hello/ready, see session.js) and
6
+ * falls back to the v1 register on the SAME connection when the gateway does not
7
+ * answer `ready` or answers `protocol_unsupported`.
8
+ *
9
+ * Unset/empty/unrecognised = false (an unknown value must never switch the transport).
10
+ */
11
+ export const PROTOCOL_V2_FLAG = "LOCAL_AGENT_PROTOCOL_V2";
12
+
13
+ const TRUE = new Set(["true", "1", "yes", "on"]);
14
+ const FALSE = new Set(["", "false", "0", "no", "off"]);
15
+
16
+ export function resolveProtocolV2Config(env = process.env, warn = console.warn) {
17
+ const raw = String(env[PROTOCOL_V2_FLAG] ?? "").trim().toLowerCase();
18
+ if (TRUE.has(raw)) return { enabled: true };
19
+ if (!FALSE.has(raw)) warn(`[la-v2] unrecognised ${PROTOCOL_V2_FLAG} value; protocol v2 stays off`);
20
+ return { enabled: false };
21
+ }
22
+
23
+ /** The per-machine authorization id the gateway checks as `agentId` (config.json `authorizationId`). */
24
+ export function resolveAgentId(env = process.env, fileCfg = {}) {
25
+ const raw = String(env.LOCAL_AGENT_ID || fileCfg.authorizationId || "").trim().toLowerCase();
26
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(raw) ? raw : null;
27
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * content.js - sends content to the control plane as a stream of `content.response` chunks (the relay of
3
+ * the design: never persisted or logged by the CP). TRANSPORT ONLY: no handler of `content.request` lives
4
+ * here (F3-07) and nothing here decides what may leave the machine (DLP is F3-09, before this is called).
5
+ *
6
+ * Rules (contract): <= 32 KiB raw per chunk (43692 base64 chars, frame <= 48 KiB), <= 64 chunks (2 MiB per
7
+ * response), chunk.index from 0, total constant, last only on the final one, byteLength = decoded size.
8
+ * Each chunk is its own data frame: numbered, acked, retransmitted. At most `maxInFlight` chunks are in
9
+ * flight and the next one is not even read until a slot opens (bounded memory for any size).
10
+ */
11
+ import { ProtocolError } from "./errors.js";
12
+ import { chunkCount } from "./chunks.js";
13
+
14
+ /**
15
+ * @param {import("./session.js").V2Session} session
16
+ * @param {object} o
17
+ * @param {string} o.requestId the content.request being answered (lower-case UUID)
18
+ * @param {Buffer | {size:number, read:(offset:number, length:number)=>Promise<Buffer>}} o.source
19
+ * @param {string} [o.contentType] one of the contract's content types
20
+ * @param {string} [o.workspaceId]
21
+ * @param {AbortSignal} [o.signal] abort stops reading and drops the chunks still waiting for a slot
22
+ * @returns {Promise<{chunks:number, bytes:number}>} resolves when the last chunk was acked
23
+ */
24
+ export async function sendContent(session, { requestId, source, contentType = "application/octet-stream", workspaceId, signal }) {
25
+ const size = Buffer.isBuffer(source) ? source.length : source.size;
26
+ if (!Number.isSafeInteger(size) || size < 0) throw new ProtocolError("internal", "source size");
27
+ const { chunkRawBytes, maxChunks } = session.limits;
28
+ const total = chunkCount(size, chunkRawBytes);
29
+ if (total > maxChunks) throw new ProtocolError("payload_too_large", "response above the chunk limit");
30
+ const acks = [];
31
+ try {
32
+ for (let index = 0; index < total; index++) {
33
+ if (signal && signal.aborted) throw new ProtocolError("agent_offline", "cancelled");
34
+ const offset = index * chunkRawBytes;
35
+ const length = Math.min(chunkRawBytes, size - offset);
36
+ const bytes = Buffer.isBuffer(source) ? source.subarray(offset, offset + length) : await source.read(offset, length);
37
+ if (!Buffer.isBuffer(bytes) || bytes.length !== length) throw new ProtocolError("internal", "source returned a wrong length");
38
+ const h = await session.enqueue("content.response", {
39
+ status: "ok", contentType, encoding: "base64", data: bytes.toString("base64"), byteLength: length, totalBytes: size,
40
+ }, { requestId, workspaceId, chunk: { index, total, last: index === total - 1 } });
41
+ acks.push(h.acked);
42
+ }
43
+ } catch (e) {
44
+ session.cancelWaiting(requestId);
45
+ for (const a of acks) a.catch(() => {});
46
+ throw e;
47
+ }
48
+ await Promise.all(acks);
49
+ return { chunks: total, bytes: size };
50
+ }
51
+
52
+ /** A failed answer: one empty chunk with the status (error | not_found | forbidden) and a wire error code. */
53
+ export async function sendContentFailure(session, { requestId, status = "error", errorCode = "internal", workspaceId }) {
54
+ await session.send("content.response", {
55
+ status, errorCode, encoding: "base64", data: "", byteLength: 0,
56
+ }, { requestId, workspaceId, chunk: { index: 0, total: 1, last: true } });
57
+ }
@@ -0,0 +1,81 @@
1
+ {
2
+ "repo": "VISIONHAMMER/vh-api-gateway",
3
+ "path": "docs/contracts/la-protocol-v2",
4
+ "branch": "release/qa",
5
+ "commit": "b090144e19b1da0182554553b18f321452f03bd6",
6
+ "branchTip": "7043f75e3ed1b201e2c958fd52f721b5a1177efe",
7
+ "note": "Copied by scripts/sync-la-protocol-v2-contract.mjs. Do not edit: change the gateway folder and sync again.",
8
+ "files": {
9
+ "README.md": "41dd3cbceccb487ca30da5838841f04d8abffcc81c31424da833114ab646b291",
10
+ "ack.schema.json": "d6dfdaa8b1ec2dd47db9e858cd047ca860196ab6b0ad350f8ccb5d2596441ccf",
11
+ "alert.schema.json": "01a8a77840be8ab7901ed7d07c3d68a2295bfaeb638694564e19e60be1747054",
12
+ "approval.decision.schema.json": "a9c7bc74d146465afde1d5830839165a02f232b830b5f6cfd8c8e91fb6da1563",
13
+ "audit.event.schema.json": "e9ceb58ea00077151373955957b8319324de488c80da4063f6d0dcb40a8b3ed0",
14
+ "config.update.schema.json": "f5318f24230f542c76a3cb996d08f33210c0b2cc2fc55ad73c4f95648582033f",
15
+ "content.request.schema.json": "9966b55ec051832ac93ee43e9cd226e4f0efaeb49741947bfc48193e40e00a32",
16
+ "content.response.schema.json": "c9a66ac9cbbcaec7fc182b3e3fa23b34877bd342592521c7e393779230332231",
17
+ "defs.schema.json": "f19fd27fd103d8da02de4506c086263c6236250c5c127aa3aab41833a90b743a",
18
+ "envelope.schema.json": "670fb26c7d532475da144185e5100b3a2ffe0d267451b1ef9debadf31b33ab79",
19
+ "error.schema.json": "b1fd663d4a1ea73d88cb575422f15f8c7279001f9385ebb2d5e1889b5d5ef878",
20
+ "heartbeat.schema.json": "8aff84a1ef3ca56bdff8819abb3b0ab01a2547814c2ea2f9ee893cb08f7a08f1",
21
+ "hello.schema.json": "bd500320f01ff10da49ded6f99c7e8081252019984afb3c178e27003df20b963",
22
+ "incident.candidate.schema.json": "b292a85917b5958fc6de10a2895732e30a35420385623930c78e47341862dc62",
23
+ "index.json": "55543ecf492b1ffe2b9a78075890ee05518d3539fbcaeb04e8b2da8ff37c9130",
24
+ "job.assign.schema.json": "fdebb4ae98bf38a2c723281c1cf9477f71d877171df9a55b0baeb0b65244f60c",
25
+ "job.cancel.schema.json": "40ef64373283e17306d857c9a40081689786015294b0b88ab8f68f063bd2cfb8",
26
+ "job.event.schema.json": "493d08f9db4f7ffd4a97403ee33ca4a73a65dc8f774d8f18ef0a137fdb5fc470",
27
+ "job.result.schema.json": "9cbc61211482557283b8f4df44e79011ead1abbacdb2d986138199c747edc043",
28
+ "log.stats.schema.json": "4b33b111946a19793a60756793294e90911b06f7050e48821759f2497ec17641",
29
+ "ready.schema.json": "6e8bd8e1a5ce1b840d35af911ae09b8f58ad9de59eb33cd2b7777859892813f4",
30
+ "register.schema.json": "e75a4aa9d5d750e1473f71ab2f37d8e9a82fcfab3e8ff1c35118d0c08916e795",
31
+ "scan.upload.schema.json": "b1bba86ee362f73afc94c8c216977c12f67f770490e81002d14789397692f23b",
32
+ "scan.uploaded.schema.json": "f4c6aed7b9363b3ce1a9e14f9c4f4fb003bcf104b60acf74421dd0c75fc5d229",
33
+ "webhook.forward.schema.json": "33d4ee238855339850ff198d1078087f3b2f48c8f290b71def93cc02dd32c985",
34
+ "vectors/ack.vectors.json": "242ec2b8ff0c748eaaedce63e0015cf5bd9a7092d1055b0670ce6b95403b54b2",
35
+ "vectors/alert.vectors.json": "0bf179ff390bbecf07a117b80237cb7ac8778864e5780f36d68c8c9a64db8f04",
36
+ "vectors/approval.decision.vectors.json": "338115973bf2c4390d2bb33ce8f4079f3132cd739cd5505e1e873fdc252d2271",
37
+ "vectors/audit.event.vectors.json": "d05c1820103eef041a84eb8b13d9a67a8a5b54b9b54ee3b6e43fd4ef15ed7000",
38
+ "vectors/config.update.vectors.json": "8a5088d547e3eb4e460b25a5e413caaa02d4402a601340e279166d8c17758e1c",
39
+ "vectors/content.request.vectors.json": "6922748edfc630ce881a1b79d24827de03f849e05c441ef7eecc684bf2238e58",
40
+ "vectors/content.response.vectors.json": "35ef18edbcec24c029781e97d60dcb0b9111697fa3a5a2a8dfd159744e7f8258",
41
+ "vectors/cross-type.vectors.json": "f7ed403d3e3f53150b715a5423fde1bed708e8c6245709ed4ad74389dc8c3307",
42
+ "vectors/error.vectors.json": "f5fd7954f2c1736ee0943eb330c2f85b7fdd7a8959e4f043f9cc9e9bcda39aa5",
43
+ "vectors/heartbeat.vectors.json": "cfd2e048139c67aa925f1fa94c05326e75248839645c1a1e4d6d3bb60cafa624",
44
+ "vectors/hello.vectors.json": "51067dc645a2e9d8116997b36e6f3fe36401d27f3ee3c18de32ec30f8a052c5f",
45
+ "vectors/incident.candidate.vectors.json": "0dd75ef396bf7b7911d20b3ab220469701f07866bb032ee31af58523c3633ab6",
46
+ "vectors/job.assign.vectors.json": "e510c2cc6d17238d477429c255d828c0339b504e39a3a50d4b6ca980e262d9ef",
47
+ "vectors/job.cancel.vectors.json": "0fc9f99c8662435e9425389068adf418273489dc73928a21eae1b283256b9bc5",
48
+ "vectors/job.event.vectors.json": "03a2a2435918ba561821916bf4e435f2780210900139ecf68091acac6abf200c",
49
+ "vectors/job.result.vectors.json": "05995fbe65e5357705d75e9b660b517f54c6902069d36b62190b1b79f283e7a5",
50
+ "vectors/log.stats.vectors.json": "a8a67e09ef0f87b81f4028b77427ed7afd809c3f8534b8423a712b2ceef9a27f",
51
+ "vectors/ready.vectors.json": "a584da17e714a13c3583256773ef0be007660c33c62a456e27568def391e7cef",
52
+ "vectors/register.vectors.json": "e2a696efc547aa2f7242128d1a860238954f9e9a1330f10b567122d7b3da0365",
53
+ "vectors/scan.upload.vectors.json": "e8a6f73fe91ac58a3222b68cf28124c1254aa4f00c3ef8f472deb7de31cab76e",
54
+ "vectors/scan.uploaded.vectors.json": "7314ea15b1afa7b9625048fb7e245492d8910bc0dae8a52ea62afe2a1e240382",
55
+ "vectors/traces/invalid-agent-mismatch.json": "5f17b867de71c1bcc6c42723acb53a50338c0e491193499ea25c2220ea4fa558",
56
+ "vectors/traces/invalid-chunk-after-last.json": "0dcc3f3cce605a490d65e96592668381fa9a191d908a26ede998717494eb2f2e",
57
+ "vectors/traces/invalid-chunk-byte-length-mismatch.json": "d328ad05b22d3abaf0497ee8482a07a08ac90a4ede94edf754e8962ca8ea201e",
58
+ "vectors/traces/invalid-chunk-last-flag.json": "6c59a361313e966999ec25c80733758d86befb5f3607528634a148291844d212",
59
+ "vectors/traces/invalid-chunk-out-of-order.json": "c6560290755d86b524316823d32fcbd1420c170b13e198813c3b7e7b7f32ecb6",
60
+ "vectors/traces/invalid-chunk-total-changes.json": "9b7ade197252275725bc0754e40eadc12a289c063fed13a2905c55583a27a767",
61
+ "vectors/traces/invalid-clock-skew.json": "de16e0bd72e08bb03d44d5f2f04a238dcd38447ff1690f8acfe712ca3fe2028b",
62
+ "vectors/traces/invalid-event-before-ready.json": "17081f197fab5f4490b15a088f2bce620921007b5fdd7ed63bd988899d674883",
63
+ "vectors/traces/invalid-first-frame-not-register.json": "cf41c2d0e67f6df382790dab13bb9e49db99a34a88c1f9d108214be5df1e4ea3",
64
+ "vectors/traces/invalid-frame-over-48kib.json": "912e37c9a326ad92284ba22af752269cb5dd52ed06cdc22b17fd3b74a8423734",
65
+ "vectors/traces/invalid-la-sends-job-assign.json": "4ce5313e125e2afa920e923c2b5b524f627f47d06961a250a8dc642b79225252",
66
+ "vectors/traces/invalid-params-raw-differs-from-payload.json": "1220630372c78a41d77b2653b8f9025cb87d601c199adb03edbf007e6e727ad1",
67
+ "vectors/traces/invalid-payload-above-limit.json": "99f3834cfea69bc6ffa169bd6a2ed915377331205f8e991ceacbf21b3525fe96",
68
+ "vectors/traces/invalid-resume-restarts-seq.json": "795964093051511c00538f88774956b471a35338b92a0a180bedb33d460f2d83",
69
+ "vectors/traces/invalid-seq-gap.json": "9c85719b07cb7309752ffaa33b6c6cb1b278c45cd72e21dbc788c56af5e4502f",
70
+ "vectors/traces/invalid-seq-repeated.json": "4a4dd18d5b9de61a6cec735229b7991d6de4f3598acd7b16541eaca44be4556d",
71
+ "vectors/traces/invalid-sig-tenant-mismatch.json": "5d8895cbc9c1b91be31a851d009d1f2b832f9735c3713b131ac9837c90b4a8e7",
72
+ "vectors/traces/invalid-signature-warn-with-enforce-required.json": "71b5b3dc5dce52417173934db1e0de6542ecae5798e87124d54a8017493312e1",
73
+ "vectors/traces/invalid-tenant-mismatch.json": "dcc04ea423bddef20524ead7bfd146a5d9c42fe474c9216dc38c90544d291151",
74
+ "vectors/traces/invalid-type-outside-allowlist.json": "e492d326cfa044d3fc9f7e48dee63a52b715a62ba4867fc5ce21f1d86e3c435f",
75
+ "vectors/traces/valid-content-chunks-in-order.json": "c5fc301527b062b30c625da2798c7cfab3e3debac29a36f6ec7c699705267ab3",
76
+ "vectors/traces/valid-handshake-and-job.json": "9aa3c423c496403ca77d63965f1805f0a57ea9f6f8a01351310515b265aeb9cc",
77
+ "vectors/traces/valid-resume-continues-seq.json": "8aa90f24598f8f2d87a5355ec58c3531d16fd11ad41c782f53eca063372b704a",
78
+ "vectors/traces/valid-retransmit-is-idempotent.json": "924e9d296a67f0006c289e7cc978b9529ae1aa03bec2cd331a62f31354a90257",
79
+ "vectors/webhook.forward.vectors.json": "38d5b53b8fb4ecdb051e2c594afc52eb8b2044e4e6aae6ffb0057fbda33bcd46"
80
+ }
81
+ }