Skip to content

M6 Phase C — AcpAdapter (ChannelIngress) + tier-1 offline proof - #28

Merged
Mayakovsky merged 1 commit into
mainfrom
m6-phase-c-acp-adapter
Jul 21, 2026
Merged

M6 Phase C — AcpAdapter (ChannelIngress) + tier-1 offline proof#28
Mayakovsky merged 1 commit into
mainfrom
m6-phase-c-acp-adapter

Conversation

@Mayakovsky

Copy link
Copy Markdown
Owner

M6 Phase C — AcpAdapter implements ChannelIngress

Builds 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.md v3 §3-C. Base: main @ 4ef8ed9 (Phase A merged + verified).

New package adapters/acp-adapter (tsc build, sweeper standalone pattern)

  • ChannelIngress conformancestart()/stop() drive the SDK agent; registerOffering records the catalog; identity(){ receivingAddress: 0xa966…e98f (Q6), did:erc8004:8453:58618 }.
  • Shared handlers (the whole point — reuse, not rebuild) — per funded job offeringHandlers[offeringId]({offeringId, requirement, jobId, buyerAddress}, deps)submit({ type:'object', value: result.payload }). import { offeringHandlers, createHandlerDeps } from '@grey/core'.
  • Ported from plugin-acp AcpService (earning path only): SSE 'entry' + poll backstop + claimDispatch dedup + "once funded, always submit, never reject" (handler throw → INSUFFICIENT_DATA deliverable) + the two-part nudge.
  • Dropped (A7) — boot-buffer (offerings register before start(); one process, no cross-plugin race), the 2s/60s PM2-restart loop (systemd Restart=on-failure + fail-fast exit), and HTTP 3001.
  • NL parser (N2/A6) — ported; emits a clean {token_address?, project_name?} (drops the _signals/raw_instruction stamps); the known-protocol regex is compiled from @grey/pipeline's canonical KNOWN_PROTOCOL_NAMES — no third divergent copy (one-line barrel export added to grey-pipeline, flagged below).
  • infra/systemd/grey-acp-adapter.service — mirrors grey-sweeper.service; ships installed-but-DISABLED; no HTTP surface.

Two safety seams

  • OBSERVE_ONLY (FDQ-63) — the first thing handleJobCreated/handleJobFunded do; 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 by if(this.reputationGate); never hard-imported. Null in Phase C → exact "no gating". C′ wires a real impl to Phase B's grey_two tables.

Deliberate build posture (deviation — flagged)

The @virtuals-protocol/acp-node-v2 SDK is a runtime-only external, loaded in one file (src/sdk.ts) via a variable-specifier dynamic import() 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 injected AcpSdkBundle seam (sweeper *Like pattern); main.ts builds the real one, tests inject a fake. Consequence: the SDK is not a package.json dependency — 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/handlers subpath question: importing @grey/core pulls 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/handlers subpath is a reasonable future footprint trim — proposed, not taken.)

FDQ-63 — verified against the ACP SDK dist (tier-2 safety pre-check #1)

acpAgent.jscreate() (: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 are createJob/setBudget/submit, none reachable from create/start/hydrate. → observe-only tier-2 is on-chain-safe iff the gate suppresses setBudget/submit/reject and covers hydration — which the OBSERVE_ONLY guard (top of both job handlers) does. Confirmed in code + tests.

Verification (ran locally)

  • typecheck 14/14 · lint 7/7 · test 7/7 (full monorepo).
  • Adapter: 21 tests — parser 9; adapter 12 (interface + identity, register, start/stop, tier-1 funded delivery via the real shared handler, observe-only suppression ×2, dedup, accept→setBudget, unparseable→reject, handler-error→INSUFFICIENT_DATA, pre-submit re-check skip, poll backstop dispatch). grey-core 96 unchanged.
  • Tier-1 offline smoke — PASS (pnpm -F @grey/acp-adapter tier1-smoke): synthetic job.funded → parse → shared legitimacy_scan (cache hit, offline) → {type:'object', value:{verdict:PASS, projectName:Uniswap, tokenAddress:0x1f98…f984, structuralScore:4}}. No chain, no wallet, no SDK.
  • Adapter dist builds with no SDK present (sdk.js keeps await import(spec) unresolved; .js extensions 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 of KNOWN_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

…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>
@Mayakovsky
Mayakovsky merged commit 7587dc3 into main Jul 21, 2026
3 checks passed
@Mayakovsky
Mayakovsky deleted the m6-phase-c-acp-adapter branch August 14, 2026 01:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant