M6 Phase C — AcpAdapter (ChannelIngress) + tier-1 offline proof - #28
Merged
Conversation
…ier-1 proven)
Movement 6 Phase C. Build `AcpAdapter implements ChannelIngress` as a standalone process that
earns through grey-core's SHARED offering handlers, and prove it offline WITHOUT becoming the
seller (tier 1). No cutover, no creds move, no chain (that's Phase D).
New package `adapters/acp-adapter` (tsc build, sweeper standalone pattern):
- ChannelIngress conformance: start()/stop() drive the SDK agent; registerOffering records the
catalog; identity() → { receivingAddress: 0xa966… (Q6), did:erc8004:8453:58618 }.
- Shared handlers (the whole point): offeringHandlers[offeringId]({offeringId,requirement,jobId,
buyerAddress}, deps) → submit { type:'object', value: result.payload }. Reuse, not rebuild.
- Ported from plugin-acp AcpService (earning path only): SSE 'entry' + poll backstop + claimDispatch
dedup + "once funded, always submit, never reject" + two-part nudge.
- Dropped (A7): boot-buffer (offerings register before start — no cross-plugin race), the 2s/60s
PM2-restart loop (systemd Restart=on-failure + fail-fast exit), HTTP 3001.
- NL parser (N2/A6): ported, emits a CLEAN {token_address?,project_name?}; known-protocol regex
compiled from @grey/pipeline's canonical KNOWN_PROTOCOL_NAMES (no third divergent copy — added a
one-line barrel export to grey-pipeline).
- OBSERVE_ONLY (FDQ-63): the FIRST thing handleJobCreated/handleJobFunded do — suppresses every
signing path (setBudget/submit/reject/nudge), covering hydration-fired entries.
- reputationGate (B6): nullable injected collaborator, every call guarded by if(this.gate); never
hard-imported. Null in Phase C → exact "no gating".
- infra/systemd/grey-acp-adapter.service (mirrors grey-sweeper; installed-but-DISABLED; no HTTP).
Deliberate build posture (flagged in the PR + README): the @virtuals-protocol/acp-node-v2 SDK is a
RUNTIME-ONLY external, loaded via a variable-specifier dynamic import in one file (src/sdk.ts) so
tsc never resolves it. The adapter core, its tests, the tier-1 smoke, and the dist build need NONE
of the SDK's heavy transitive tree (@account-kit/@alchemy/@privy/socket.io — the M5 VPS OOM). The
adapter reaches the SDK only through the injected AcpSdkBundle seam.
FDQ-63 verified against the actual SDK dist (acpAgent.js:24-34 create / :94-102 start / :113-124
hydrateSessions): create+start+hydrate do ONLY reads (getAddress/getContractAddresses/getActiveJobs/
getHistory/fetchJob) + SSE subscribe + fire the entry handler on pre-existing jobs at startup — zero
on-chain write. The only writes are createJob/setBudget/submit, none reachable from create/start.
→ observe-only tier-2 is on-chain-safe behind the OBSERVE_ONLY gate covering hydration.
Green: typecheck 14/14, lint 7/7, test 7/7 (adapter 21: parser 9 + adapter 12 incl. tier-1 funded
delivery, observe-only suppression, dedup, accept, error-fallback, poll backstop). Tier-1 offline
smoke PASS (synthetic job.funded → parse → shared legitimacy_scan → {type:object,value:{verdict:PASS,
…}}). grey-core 96 unchanged. Adapter dist builds with no SDK present.
Not in this phase: tier-2 live run (gated, post-safety-report + go), any cutover/creds/seller action
(Phase D), the reputation gate impl (C′). New FDQs: none (ledger open at FDQ-67).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M6 Phase C —
AcpAdapter implements ChannelIngressBuilds the ACP marketplace as adapter #2 over grey-core's channel-agnostic core, and proves it offline without becoming the seller (tier 1). No cutover, no creds move, no chain — that is Phase D. Spec:
movement-6-BUILD-SPEC-KOV.mdv3 §3-C. Base:main @ 4ef8ed9(Phase A merged + verified).New package
adapters/acp-adapter(tsc build, sweeper standalone pattern)start()/stop()drive the SDK agent;registerOfferingrecords the catalog;identity()→{ receivingAddress: 0xa966…e98f (Q6), did:erc8004:8453:58618 }.offeringHandlers[offeringId]({offeringId, requirement, jobId, buyerAddress}, deps)→submit({ type:'object', value: result.payload }).import { offeringHandlers, createHandlerDeps } from '@grey/core'.AcpService(earning path only): SSE'entry'+ poll backstop +claimDispatchdedup + "once funded, always submit, never reject" (handler throw → INSUFFICIENT_DATA deliverable) + the two-part nudge.start(); one process, no cross-plugin race), the 2s/60s PM2-restart loop (systemdRestart=on-failure+ fail-fast exit), and HTTP 3001.{token_address?, project_name?}(drops the_signals/raw_instructionstamps); the known-protocol regex is compiled from@grey/pipeline's canonicalKNOWN_PROTOCOL_NAMES— no third divergent copy (one-line barrel export added to grey-pipeline, flagged below).infra/systemd/grey-acp-adapter.service— mirrorsgrey-sweeper.service; ships installed-but-DISABLED; no HTTP surface.Two safety seams
OBSERVE_ONLY(FDQ-63) — the first thinghandleJobCreated/handleJobFundeddo; suppresses every signing path (setBudget/submit/reject/nudge), covering hydration-fired entries. Read-only observation (fetch+parse+log) still runs so tier-2 sees traffic without signing.reputationGate(B6) — nullable injected collaborator, every call guarded byif(this.reputationGate); never hard-imported. Null in Phase C → exact "no gating". C′ wires a real impl to Phase B'sgrey_twotables.Deliberate build posture (deviation — flagged)
The
@virtuals-protocol/acp-node-v2SDK is a runtime-only external, loaded in one file (src/sdk.ts) via a variable-specifier dynamicimport()so tsc never statically resolves it. The adapter core, its unit tests, the tier-1 smoke, and the dist build need none of the SDK's heavy transitive tree (@account-kit/@alchemy/@privy-io/socket.io— the tree that OOM'd the 1.9 GB VPS in M5). The adapter reaches the SDK only through the injectedAcpSdkBundleseam (sweeper*Likepattern);main.tsbuilds the real one, tests inject a fake. Consequence: the SDK is not apackage.jsondependency — it is installed on the box at deploy (filtered + swap-armed) exactly as the ElizaOS agent has it. Building the dist needs none of it. (Footprint note re the directive's@grey/core/handlerssubpath question: importing@grey/corepulls fastify via its index; that is negligible vs the SDK tree and the adapter replaces the far heavier ElizaOS process, so I kept the main surface. A@grey/core/handlerssubpath is a reasonable future footprint trim — proposed, not taken.)FDQ-63 — verified against the ACP SDK dist (tier-2 safety pre-check #1)
acpAgent.js—create()(:24-34),start()(:94-102),hydrateSessions()(:113-124): create/start/hydrate perform only reads (getAddress/getContractAddresses/getSupportedChainIds/api.getActiveJobs/transport.getHistory/session.fetchJob) + SSE subscribe, and fire the entry handler on pre-existing active jobs at startup. The only on-chain writes arecreateJob/setBudget/submit, none reachable fromcreate/start/hydrate. → observe-only tier-2 is on-chain-safe iff the gate suppressessetBudget/submit/rejectand covers hydration — which theOBSERVE_ONLYguard (top of both job handlers) does. Confirmed in code + tests.Verification (ran locally)
pnpm -F @grey/acp-adapter tier1-smoke): syntheticjob.funded→ parse → sharedlegitimacy_scan(cache hit, offline) →{type:'object', value:{verdict:PASS, projectName:Uniswap, tokenAddress:0x1f98…f984, structuralScore:4}}. No chain, no wallet, no SDK.sdk.jskeepsawait import(spec)unresolved;.jsextensions preserved for node ESM).Not in this PR / phase
Tier-2 live run (gated — awaits this FDQ-63 report + a go); any cutover/creds-move/seller action (Phase D); the reputation gate impl (C′). New FDQs: none (ledger open at FDQ-67).
Flagged enabling change outside the adapter
packages/grey-pipeline/src/index.ts— added a barrel export ofKNOWN_PROTOCOL_NAMES/KNOWN_PROTOCOL_PATTERN/buildProtocolPattern(additive) so the parser sources the canonical list instead of a third copy.HOLD for merge. On merge: C′ (shadow gate → Phase B tables) and/or the gated tier-2, then Phase D (cutover), as Forces sequences. Filtered on-box dist-build confirmation (memory-checked) can follow on the box — the dist needs no SDK, so it is memory-light.
🤖 Generated with Claude Code