Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions .agent/context/20260908T224457Z-production-recovery-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Session Context: production recovery configuration

## Date/time

- UTC: 2026-09-08T22:44:57Z

## User goal

Fix blockers left by earlier plans before continuing future work, using provider access already held in Google Cloud, Privy, and The Graph.

## Original prompt/request

"let's fix current issues that we have from previous plans, after contining future work. all of api's i have in google cloud, privy, the graph and etc."

## Assumptions

- Credentials and API secrets remain outside Git and review prompts.
- The current packet fixes the production recovery configuration boundary before any live MCP/model adapter or evidence claim.
- A single configured sender and bounded block window are sufficient for the current live-value gate; per-intent window derivation is future work if multi-intent production recovery needs it.

## Plan

1. Remove placeholder Graph identity and unbounded recovery correlation values.
2. Require explicit production recovery lookup configuration and reject invalid input before MCP lookup.
3. Run repository checks and mandatory FreePi review gates, then open a draft PR.

## Key decisions

- Reused reconciliation's existing validation predicates through one exported boolean; no duplicate validator and no new dependency.
- Kept Subgraph MCP and advisor unavailable by default. Live admission still requires the recorded C01 promotion evidence.
- Used an options object for production recovery composition so provider ports and lookup configuration cannot be positionally confused.

## Files/components touched

- `packages/reconciliation/src/validation.ts`: reusable lookup-input validation.
- `packages/reconciliation/src/index.ts`: public validation export.
- `apps/worker/src/recovery-bridge.ts`: explicit real lookup configuration; placeholder removal; fail-closed validation.
- `apps/worker/src/composition.ts`: required production recovery options.
- `apps/worker/test/p4-composition.test.ts`: valid identities, propagation, and placeholder rejection coverage.

## Commands/checks

- `pnpm --filter @oneshot/reconciliation typecheck` - PASS; local Node 22 warning against pinned Node 24.19.0.
- `pnpm --filter @oneshot/worker typecheck` - PASS; same engine warning.
- `pnpm --filter @oneshot/worker test -- --run test/p4-composition.test.ts` - PASS, 7 tests after fixture correction.
- `pnpm --filter @oneshot/reconciliation test` - PASS, 74 tests.
- `pnpm lint` - PASS.
- `pnpm typecheck` - PASS.
- `pnpm test` - PASS, 54 files and 868 tests; includes full build.
- `pnpm format:check` - FAIL only on two pre-existing generated subgraph files; all five touched TypeScript files pass targeted Prettier check.
- `git diff --check` - PASS.

## External-doc findings

- The Graph official `graphops/subgraph-mcp` documentation confirms immutable queries use `execute_query_by_deployment_id` with deployment ID, query, and variables.
- MCP 2025-11-25 schema confirms `tools/call` is JSON-RPC 2.0 with a tool name and arguments.
- Google Cloud Vertex AI documentation confirms REST `generateContent` bearer authentication and JSON structured output support. No adapter is admitted in this packet.

## Unresolved questions

- Canonical live deployment ID, manifest CID, MCP endpoint/version, sender, and bounded Arc block window still need retrieval from operator-controlled systems.
- Live Subgraph MCP and Vertex AI model traces remain required before `SELECT_SUBGRAPH_MCP` or sponsor qualification.

## Git and PR state

- Branch: `fix/production-recovery-config`
- Base: `origin/develop` at `48391e4968675764632627716e580988a271c13d`
- Commit: uncommitted
- PR: not created
- CI: not run

## Review gates

- Gate A: NOT RUN
- Gate B: NOT RUN

## Handoff/next steps

1. Finish local checks and Gate A.
2. Commit, push, open draft PR, wait for exact-head CI, and run Gate B.
3. After human merge, retrieve non-secret live identities and implement/admit the minimal MCP/model adapters only with live evidence.
26 changes: 16 additions & 10 deletions apps/worker/src/composition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,32 @@ import {
IntentLedgerLocalRecoveryStatePort,
IntentLedgerRecoveryCommandStore,
PrivyArcEvidenceBridge,
type IntentLedgerLocalRecoveryStatePortOptions,
type PrivyArcEvidenceBridgeOptions,
} from './recovery-bridge.js';

export const CURRENT_CONTRACT_VERSION = '1.0.0';
export const SUPPORTED_NETWORK = 'eip155:5042002';

export interface ProductionRecoveryServiceOptions {
readonly localState: IntentLedgerLocalRecoveryStatePortOptions;
readonly bridge?: PrivyArcEvidenceBridgeOptions;
readonly subgraphMcp?: SubgraphMcpRecoveryPort;
readonly advisor?: RecoveryAdvisorPort;
}

export function createProductionRecoveryService(
ledger: IntentLedger,
bridgeOptions?: PrivyArcEvidenceBridgeOptions,
subgraphMcpPort?: SubgraphMcpRecoveryPort,
advisor?: RecoveryAdvisorPort,
options: ProductionRecoveryServiceOptions,
): RecoveryService {
const localState = new IntentLedgerLocalRecoveryStatePort(ledger);
const localState = new IntentLedgerLocalRecoveryStatePort(ledger, options.localState);
const commandStore = new IntentLedgerRecoveryCommandStore(ledger);
const knownIdentityEvidence = new PrivyArcEvidenceBridge({
localStatePort: localState,
...bridgeOptions,
...options.bridge,
});
const subgraphMcp = subgraphMcpPort ?? new UnavailableSubgraphMcpRecoveryPort();
const recoveryAdvisor = advisor ?? new UnavailableRecoveryAdvisorPort();
const subgraphMcp = options.subgraphMcp ?? new UnavailableSubgraphMcpRecoveryPort();
const recoveryAdvisor = options.advisor ?? new UnavailableRecoveryAdvisorPort();
return new RecoveryService({
localState,
knownIdentityEvidence,
Expand Down Expand Up @@ -109,7 +115,7 @@ export interface CompositionOptions {
readonly contractVersion?: string;
};
readonly recoveryService?: RecoveryService;
readonly recoveryBridgeOptions?: PrivyArcEvidenceBridgeOptions;
readonly recovery?: ProductionRecoveryServiceOptions;
readonly submissionsDisabled?: boolean;
readonly expectedContractVersion?: string;
readonly expectedNetwork?: string;
Expand Down Expand Up @@ -143,8 +149,8 @@ export function composeWorker(
}

let recoveryService = options.recoveryService;
if (!recoveryService && options.profile === 'production' && options.recoveryBridgeOptions) {
recoveryService = createProductionRecoveryService(ledger, options.recoveryBridgeOptions);
if (!recoveryService && options.profile === 'production' && options.recovery) {
recoveryService = createProductionRecoveryService(ledger, options.recovery);
}

const workerOptions: WorkerOptions = {
Expand Down
37 changes: 15 additions & 22 deletions apps/worker/src/recovery-bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import type { IntentLedger } from '@oneshot/storage-postgres';
import {
APPEND_RECOVERY_RECORD_VERSION,
isValidSubgraphLookupInput,
LOCAL_RECOVERY_SNAPSHOT_VERSION,
RECOVERY_EVIDENCE_VERSION,
type EvidenceBinding,
Expand Down Expand Up @@ -56,8 +57,11 @@ function toContractAuthorityClass(authClass: string): 'AUTHORITATIVE' | 'OBSERVA
}

export interface IntentLedgerLocalRecoveryStatePortOptions {
readonly tokenContract?: string;
readonly correlationSender?: string;
readonly tokenContract: string;
readonly correlationSender: string;
readonly fromBlock: string;
readonly toBlock: string;
readonly mcpPolicy: SubgraphMcpPolicy;
}

/**
Expand All @@ -66,7 +70,7 @@ export interface IntentLedgerLocalRecoveryStatePortOptions {
export class IntentLedgerLocalRecoveryStatePort implements LocalRecoveryStatePort {
constructor(
private readonly ledger: IntentLedger,
private readonly options: IntentLedgerLocalRecoveryStatePortOptions = {},
private readonly options: IntentLedgerLocalRecoveryStatePortOptions,
) {}

async read(businessIntentId: string): Promise<LocalRecoverySnapshot> {
Expand All @@ -75,16 +79,11 @@ export class IntentLedgerLocalRecoveryStatePort implements LocalRecoveryStatePor
throw new Error(`Intent not found: ${businessIntentId}`);
}

const tokenContract =
this.options.tokenContract ??
(intent.attempts?.[0] as { token_contract?: string } | undefined)?.token_contract ??
'0x3333333333333333333333333333333333333333';

const binding: EvidenceBinding = {
businessIntentId: intent.business_intent_id,
requestFingerprint: intent.payload_fingerprint,
network: intent.network,
tokenContract,
tokenContract: this.options.tokenContract,
recipient: intent.recipient,
amountAtomic: intent.amount_atomic,
};
Expand All @@ -96,21 +95,15 @@ export class IntentLedgerLocalRecoveryStatePort implements LocalRecoveryStatePor
binding,
correlation: {
strategy: 'TRANSFER_TUPLE_WINDOW',
sender: '0x2222222222222222222222222222222222222222',
fromBlock: '0',
toBlock: 'latest',
sender: this.options.correlationSender,
fromBlock: this.options.fromBlock,
toBlock: this.options.toBlock,
},
};

const mcpPolicy: SubgraphMcpPolicy = {
serverName: 'subgraph-mcp',
serverVersion: '1.0.0',
deploymentId: 'oneshot-arc-testnet',
manifestCid: 'QmOneShotArcTestnetManifest',
maxLagBlocks: '50',
maxCandidates: 5,
maxResultBytes: 65536,
};
if (!isValidSubgraphLookupInput(indexRequest, this.options.mcpPolicy)) {
throw new Error('Invalid Subgraph MCP recovery lookup input');
}

return {
schemaVersion: LOCAL_RECOVERY_SNAPSHOT_VERSION,
Expand All @@ -122,7 +115,7 @@ export class IntentLedgerLocalRecoveryStatePort implements LocalRecoveryStatePor
persistedAt: nowIso,
},
indexRequest,
mcpPolicy,
mcpPolicy: this.options.mcpPolicy,
capturedAt: nowIso,
};
}
Expand Down
Loading
Loading