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.
- package/.github/workflows/deploy-gcp.yml +58 -26
- package/CHANGELOG.md +13 -0
- package/docs/la-protocol-v2.md +68 -0
- package/docs/ws-metadata-only.md +44 -0
- package/env.example +7 -0
- package/package.json +2 -1
- package/scripts/sync-la-protocol-v2-contract.mjs +58 -0
- package/src/la-protocol-v2/chunks.js +45 -0
- package/src/la-protocol-v2/config.js +27 -0
- package/src/la-protocol-v2/content.js +57 -0
- package/src/la-protocol-v2/contract/CONTRACT_SOURCE.json +81 -0
- package/src/la-protocol-v2/contract/README.md +103 -0
- package/src/la-protocol-v2/contract/ack.schema.json +51 -0
- package/src/la-protocol-v2/contract/alert.schema.json +62 -0
- package/src/la-protocol-v2/contract/approval.decision.schema.json +57 -0
- package/src/la-protocol-v2/contract/audit.event.schema.json +86 -0
- package/src/la-protocol-v2/contract/config.update.schema.json +73 -0
- package/src/la-protocol-v2/contract/content.request.schema.json +110 -0
- package/src/la-protocol-v2/contract/content.response.schema.json +90 -0
- package/src/la-protocol-v2/contract/defs.schema.json +191 -0
- package/src/la-protocol-v2/contract/envelope.schema.json +89 -0
- package/src/la-protocol-v2/contract/error.schema.json +59 -0
- package/src/la-protocol-v2/contract/heartbeat.schema.json +100 -0
- package/src/la-protocol-v2/contract/hello.schema.json +111 -0
- package/src/la-protocol-v2/contract/incident.candidate.schema.json +102 -0
- package/src/la-protocol-v2/contract/index.json +166 -0
- package/src/la-protocol-v2/contract/job.assign.schema.json +212 -0
- package/src/la-protocol-v2/contract/job.cancel.schema.json +52 -0
- package/src/la-protocol-v2/contract/job.event.schema.json +115 -0
- package/src/la-protocol-v2/contract/job.result.schema.json +100 -0
- package/src/la-protocol-v2/contract/log.stats.schema.json +79 -0
- package/src/la-protocol-v2/contract/ready.schema.json +106 -0
- package/src/la-protocol-v2/contract/register.schema.json +92 -0
- package/src/la-protocol-v2/contract/scan.upload.schema.json +59 -0
- package/src/la-protocol-v2/contract/scan.uploaded.schema.json +51 -0
- package/src/la-protocol-v2/contract/webhook.forward.schema.json +69 -0
- package/src/la-protocol-v2/contract.js +90 -0
- package/src/la-protocol-v2/errors.js +34 -0
- package/src/la-protocol-v2/index.js +8 -0
- package/src/la-protocol-v2/link.js +93 -0
- package/src/la-protocol-v2/runtime.js +72 -0
- package/src/la-protocol-v2/session.js +650 -0
- package/src/la-protocol-v2/strict-json.js +47 -0
- package/src/mcp-http-server.js +5 -3
- package/src/server.js +70 -9
- package/src/utils/exec-policy.js +8 -1
- package/src/ws-channel/allowlist.js +143 -0
- package/src/ws-channel/config.js +22 -0
- package/src/ws-channel/connection-state.js +18 -0
- package/src/ws-channel/index.js +6 -0
- package/src/ws-channel/metadata-guard.js +78 -0
- package/src/ws-channel/policy-offline.js +40 -0
- package/src/ws-channel/transit-guard.js +55 -0
- package/test/contract/la-protocol-v2/vectors/ack.vectors.json +149 -0
- package/test/contract/la-protocol-v2/vectors/alert.vectors.json +101 -0
- package/test/contract/la-protocol-v2/vectors/approval.decision.vectors.json +165 -0
- package/test/contract/la-protocol-v2/vectors/audit.event.vectors.json +156 -0
- package/test/contract/la-protocol-v2/vectors/config.update.vectors.json +189 -0
- package/test/contract/la-protocol-v2/vectors/content.request.vectors.json +325 -0
- package/test/contract/la-protocol-v2/vectors/content.response.vectors.json +413 -0
- package/test/contract/la-protocol-v2/vectors/cross-type.vectors.json +260 -0
- package/test/contract/la-protocol-v2/vectors/error.vectors.json +179 -0
- package/test/contract/la-protocol-v2/vectors/heartbeat.vectors.json +109 -0
- package/test/contract/la-protocol-v2/vectors/hello.vectors.json +224 -0
- package/test/contract/la-protocol-v2/vectors/incident.candidate.vectors.json +186 -0
- package/test/contract/la-protocol-v2/vectors/job.assign.vectors.json +631 -0
- package/test/contract/la-protocol-v2/vectors/job.cancel.vectors.json +133 -0
- package/test/contract/la-protocol-v2/vectors/job.event.vectors.json +233 -0
- package/test/contract/la-protocol-v2/vectors/job.result.vectors.json +137 -0
- package/test/contract/la-protocol-v2/vectors/log.stats.vectors.json +137 -0
- package/test/contract/la-protocol-v2/vectors/ready.vectors.json +177 -0
- package/test/contract/la-protocol-v2/vectors/register.vectors.json +399 -0
- package/test/contract/la-protocol-v2/vectors/scan.upload.vectors.json +137 -0
- package/test/contract/la-protocol-v2/vectors/scan.uploaded.vectors.json +101 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-agent-mismatch.json +140 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-after-last.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-byte-length-mismatch.json +174 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-last-flag.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-out-of-order.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-chunk-total-changes.json +200 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-clock-skew.json +46 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-event-before-ready.json +72 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-first-frame-not-register.json +48 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-frame-over-48kib.json +141 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-la-sends-job-assign.json +173 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-params-raw-differs-from-payload.json +172 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-payload-above-limit.json +174 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-resume-restarts-seq.json +144 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-gap.json +166 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-seq-repeated.json +166 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-sig-tenant-mismatch.json +172 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-signature-warn-with-enforce-required.json +77 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-tenant-mismatch.json +140 -0
- package/test/contract/la-protocol-v2/vectors/traces/invalid-type-outside-allowlist.json +140 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-content-chunks-in-order.json +224 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-handshake-and-job.json +266 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-resume-continues-seq.json +168 -0
- package/test/contract/la-protocol-v2/vectors/traces/valid-retransmit-is-idempotent.json +193 -0
- package/test/contract/la-protocol-v2/vectors/webhook.forward.vectors.json +170 -0
- package/test/la-protocol-v2/contract.test.js +131 -0
- package/test/la-protocol-v2/helpers/la-rig.js +83 -0
- package/test/la-protocol-v2/helpers/reference-cp.js +349 -0
- package/test/la-protocol-v2/helpers/spawn-agent.js +35 -0
- package/test/la-protocol-v2/link.test.js +141 -0
- package/test/la-protocol-v2/session-inbound.test.js +279 -0
- package/test/la-protocol-v2/session-outbound.test.js +382 -0
- package/test/la-protocol-v2/traces.test.js +270 -0
- package/test/la-protocol-v2/tunnel-e2e.test.js +124 -0
- package/test/ws-channel/allowlist.test.js +67 -0
- package/test/ws-channel/metadata-guard.test.js +104 -0
- package/test/ws-channel/policy-offline.test.js +99 -0
- package/test/ws-channel/tunnel-e2e.test.js +88 -0
- package/tools/graphify-validation/extras/README.md +19 -0
- package/tools/graphify-validation/extras/bymethod.py +35 -0
- package/tools/graphify-validation/extras/docker/Dockerfile +16 -0
- package/tools/graphify-validation/extras/docker/inner.sh +7 -0
- package/tools/graphify-validation/extras/g10.py +59 -0
- package/tools/graphify-validation/extras/g3aug.py +49 -0
- package/tools/graphify-validation/extras/hybrid.py +34 -0
- package/tools/graphify-validation/extras/truth2.py +44 -0
- package/tools/interop/LaV2NodeInteropTest.java +266 -0
- package/tools/interop/README.md +21 -0
- package/tools/interop/la-interop-client.mjs +97 -0
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
# Deploy local-agent (Node) to GCP Cloud Run
|
|
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
|
-
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
81
|
+
run: gcloud auth configure-docker "$AR_HOST" --quiet
|
|
72
82
|
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
docker build --platform=linux/amd64 -t "$
|
|
77
|
-
docker push "$
|
|
78
|
-
|
|
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
|
|
97
|
+
- name: Deploy to Cloud Run (QA, image only by digest, live)
|
|
98
|
+
if: inputs.environment != 'prod'
|
|
81
99
|
run: |
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
$
|
|
129
|
+
--no-traffic --tag "${{ steps.vars.outputs.tag }}" \
|
|
99
130
|
--quiet
|
|
100
131
|
|
|
101
132
|
- name: Summary
|
|
102
|
-
run:
|
|
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.
|
|
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
|
+
}
|