Skip to content

fix(gateway): operator read routes stop fabricating earnings and fleet data - #362

Merged
LamaSu merged 7 commits into
masterfrom
fix/operator-routes-no-fabrication
Oct 3, 2026
Merged

LamaSu merged 7 commits into
masterfrom
fix/operator-routes-no-fabrication

Conversation

@LamaSu

@LamaSu LamaSu commented Sep 24, 2026

Copy link
Copy Markdown
Owner

The production gateway served fabricated operator data. GET /api/operator/earnings returned a new random daily earnings series on every call. /api/operator/machines, /certifications and /maintenance returned hard-coded rows (a "Prusa MK4 Workshop" at 72% utilization, OSHA certificates, a nozzle replacement).

The public agent package advertises these routes as an operator's earnings and machines (get_operator_earnings, get_operator_machines, get_operator_certs), and /api/agent/me lists them as "Earnings and what is owed". So an agent acting for an operator read invented income. This violates product invariant 3: never substitute plausible values for missing real data.

Change (branch fix/operator-routes-no-fabrication, base master ac86a404)

  • Four routes return 501 instead of invented data. They answer 501 {error:"not_available", message, see} and point to the reads that are real: /api/agent/me (the operator's actual kernels, devices and in-flight jobs) and /api/jobs/:jobId/execution (per-job payment state from the escrow record; PR feat: JobExecutionDTO read model; job detail stops reading mock data (PX-6) #353).
    • Earnings history cannot be served honestly yet, because escrow milestones carry no release time and no per-operator payout record.
    • The real operator income read model is PX-7 (OperatorWorkDTO), to be built with the operator-ux lane.
  • The policy read no longer falls back to defaults on failure. GET /api/operator/policy/:kernelId returned DEFAULT_OPERATOR_POLICY when the store read failed, so an operator's real guardrails could read as the defaults. A failed read is now 503 read_failed. Only "no saved row" means the defaults (source: "default").
  • Docs match the routes. The context pack table and the /api/agent/me route catalog now say "Not available yet (501)" and name the real reads.

Tests

The new file operator-no-fabrication.test.ts has 10 tests:

  • each route returns 501 with no fabricated fields;
  • earnings are identical across calls;
  • the pointers to real reads are present;
  • a policy with no saved row reads as the defaults;
  • a failed policy read returns 503 and never the defaults;
  • a source ratchet forbids mock arrays and random values in operator.ts (random id suffixes stay allowed).

Results:

  • Mutation check: re-adding random earnings turns 5 tests red, and restoring the policy fallback turns 1 red.
  • Full gateway suite: 2991 passed, 6 skipped, 0 failed. tsc --noEmit is clean.

Follow-ups (not in this PR)

  • The agent package's tool descriptions for these routes belong to the launch/aeo lanes (generated product facts, PX-16).
  • The same class of server-side fabrication exists in other route files (swf.ts epoch distribution uses random contribution scores; spaces.ts, marketplace.ts, rewards.ts, logistics.ts, orchestrator.ts, protocols.ts and agents.ts hold mock arrays). A per-route census is in progress.

🤖 Generated with Claude Code

https://claude.ai/code/session_01A2ZvsqsAb7jC7AC8Viisqn

…t data

GET /api/operator/earnings returned a new RANDOM daily earnings series on
every call, and /api/operator/machines, /certifications and /maintenance
returned hard-coded rows (a "Prusa MK4 Workshop" at 72% utilization, OSHA
certificates, nozzle maintenance). The public agent package advertises these
as an operator's earnings and machines (get_operator_earnings,
get_operator_machines, get_operator_certs), and /api/agent/me lists
"Earnings and what is owed", so an agent acting for an operator read invented
income. Product invariant 3: a missing fact is never a plausible number.

- The four routes now answer 501 {error:"not_available", message, see} and
  point at the reads that are real: /api/agent/me (the operator's kernels,
  devices and in-flight jobs) and /api/jobs/:jobId/execution (per-job payment
  state from the escrow record). Earnings history cannot be served honestly
  yet: escrow milestones carry no release time or per-operator payout record.
  The real operator income read model is PX-7 (OperatorWorkDTO).
- GET /api/operator/policy/:kernelId returned DEFAULT_OPERATOR_POLICY when
  the store read FAILED, so an operator's real guardrails could read as the
  defaults. A failed read is now 503 read_failed; only "no saved row" means
  the defaults (source: "default").

Tests (operator-no-fabrication.test.ts, 10): each route is 501 with no
fabricated fields; earnings are deterministic across calls; pointers present;
no-row policy = default; failed policy read = 503 and never the defaults; a
source ratchet forbids mock arrays and random VALUES in operator.ts (random id
suffixes stay allowed). Mutation-checked: re-adding random earnings -> 5 red;
restoring the policy fallback -> red. Full gateway suite 2991 passed, 6
skipped, 0 failed; tsc clean.

agent: pcc-readmodels (c255d7dc)
…eads are not available

The context pack table and the /api/agent/me route catalog described
/api/operator/{machines,earnings,certifications,maintenance} as returning an
operator's machines, "Earnings and what is owed", certifications and
maintenance windows. They now say "Not available yet (501)" and name the real
reads to use instead, so agents are not sent to routes that cannot answer.

agent: pcc-readmodels (c255d7dc)
LamaSu added a commit that referenced this pull request Sep 24, 2026
… fixtures

pcc-economics (df42dbe5), from readmodels' server-side fabrication census
(2026-09-24), which assigned this family to economics.

- POST /api/swf/epochs/:epochId/distribute scored every participant with
  Math.random() (jobs, reputation, uptime, votes) and then DISTRIBUTED the
  epoch on those scores. It now refuses with 501 not_available until real
  per-epoch contribution inputs exist; the epoch is left untouched.
- /api/rewards/*, /api/certificates* and /api/treasury/summary served
  fixtures: reward epochs and payouts, "claimed" claims with a fake tx
  hash, a mint that reported minted:true, and a 50000 USDC / 85000 USD
  treasury. All now answer 501 not_available with pointers to the real
  reads (escrow, jobs, capabilities, compliance), following PR #362's
  pattern. Paths stay registered so clients get an honest answer.
- MCP pcc_depin_stats and the CLI depin command report each part on its
  own (pccFetchEach): an unavailable part is { unavailable, reason }, never
  zero and never a thrown whole.

Tests: gateway economics-no-fabrication 10/10 (asserts no fixture value
leaks and the refused distribution leaves the epoch unchanged); full
gateway 2939 passed locally (the 2 capture files need @pcc/verifier
built; CI builds it). mcp-server 71/71, tsc clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KGRcaWqVay3ueHYRBcXJ7K
LamaSu added a commit that referenced this pull request Sep 29, 2026
…s, /api/issues or the demo API

Shell's #3266 asked whether #380's pages read GET /api/operator/{machines,
earnings,certifications,maintenance}, which still serve literals and
Math.random money on master (routes/operator.ts; readmodels #362 / N32). They
do not; this test makes that a checked fact rather than a code reading: it
renders every dashboard tab, the machine page and every mobile tab against a
stubbed gateway and fails if any request goes to those four routes, to the
nonexistent /api/issues, or to /api/demo/*. Mutation-checked: adding one fetch
of /api/operator/earnings to the dashboard turns it red.

agent: pcc-operator-ux (f0734fab)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ever an empty list

GET /api/operator/approvals caught every storage failure and answered 200 with
{approvals: []}, an empty list that hides recorded approvals during an outage.
It now answers 503 read_failed (the same refusal shape the operator policy read
uses) with no approvals key. A real empty result is still a 200 with an empty list.

Reproduced first: the new M1 tests were run against a byte-identical copy of the
d30de64 route (operator.ts blob fbf9be8). With the store closed, and with a
throwing .all() for each of the four filter combinations, the route answered 200
and both tests failed with "expected 200 to be 503". With this change they pass.

Review: PR #362 cross-family review rm-n32-362-r1-d30de649, finding M1.
agent: implementer-delta for pcc-readmodels (c255d7dc)
…Type as unknown, never liquid-handler

POST /api/operator/approvals persisted jobSummary.capabilityType as the invented
"liquid-handler" whenever the caller left capabilityType out, and then served it
back as recorded job truth. The field is optional in the spec
(PendingApproval.jobSummary.capabilityType?: string) and job_summary is a JSON
object, so an absent value can simply be absent: the key is now left out of
jobSummary unless the caller sent one. A caller-supplied value, including an
explicit "liquid-handler", is stored unchanged. No 400 is needed because the
column holds the whole JSON object and does not require this key.

Callers checked: the dashboard approvals tab reads a top-level capabilityType the
gateway never sends, tool-suggestions.ts only advertises the endpoints, and neither
relies on the default. scripts/ot2-agent.py keeps its own fallback for a missing
key (it drives a single OT-2 liquid handler).

Reproduced first: the new M2 test was run against a byte-identical copy of the
d30de64 route (operator.ts blob fbf9be8). A POST with only kernelId and agentId
stored and returned capabilityType "liquid-handler", and the test failed with
"expected { ...(2) } to not have property capabilityType" (Received
"liquid-handler"). With this change it passes.

Review: PR #362 cross-family review rm-n32-362-r1-d30de649, finding M2.
agent: implementer-delta for pcc-readmodels (c255d7dc)
…update changed nothing

The approve and reject routes restrict their update to status "pending", but they
ignored its changed-row count and then accepted any row that read back as
"approved" (or "rejected") as success. A second approve of an approved record, a
second reject of a rejected one, or an approve of an auto-approved one all
answered 200 {approved: true}, as if this request had made the decision. The
mixed cases (reject after approve, approve after reject) answered 404 with the
ambiguous "not found or already decided".

Both routes now use the update's changed-row count:
- the record does not exist: the existing 404 answer, unchanged;
- 0 rows changed and the record exists: 409 {error: "already_decided", status,
  message} carrying the record's current status, with no approved/rejected flag,
  and the record is left as it was (decidedAt and rejectionReason untouched);
- 1 row changed: success as before.

Reproduced first: the new M3 tests were run against a byte-identical copy of the
d30de64 route (operator.ts blob fbf9be8). Approve twice and reject twice both
answered 200 for the second call (expected 409), reject-after-approve and
approve-after-reject answered 404 (expected 409), and approve of an auto-approved
record answered 200 (expected 409). With this change all pass, and the unknown-id
control still answers the existing 404.

Dashboard left untouched: OperatorDashboardPage handleApprovalAction does not
check res.ok, so it treats this 409 (like any non-2xx) as a success. Flagged for a
separate change.

Review: PR #362 cross-family review rm-n32-362-r1-d30de649, finding M3.
agent: implementer-delta for pcc-readmodels (c255d7dc)
… and the 404 says only "not found" (N32)

Follow-ups to the r1 MEDIUM fixes, found by implementer-delta while
fixing them:

- A repeated ?status= or ?kernelId= arrives as a list, and the SQLite
  bind threw. Before M1 that was a 200 with an empty list; after M1 it
  was a 503 read_failed, a client error answered as an outage. It is
  now 400 invalid_query, with a test.
- After M3, an already-decided approval gets 409 already_decided. So
  the 404 on approve or reject is reachable only for a missing record.
  Its text, "Approval not found or already decided", is now
  "Approval not found".

agent: pcc-readmodels (c255d7dc)
… 501, or no route (N32)

Found while packing #362 round 2. Round 1 could not see whether the
agent package's get_operator_* tools handle the 501 refusals honestly.
They did not: the public manifest (apps/dashboard/public/
agent-package.json) and agent-package-test.json still promised
"utilization and uptime stats", "daily earnings and cumulative totals"
and "OSHA, safety training, PCC network certs". Those routes answer 501
not_available. get_operator_dashboard points at GET /api/operator,
which no route registers.

The four descriptions now say exactly that, and where the real data is:
- /api/agent/me for kernels, devices and in-flight jobs;
- /api/jobs/:jobId/execution for per-job payment state.
The certifications text says that certifications typed at registration
are the operator's own claim and are not checked.

Only these four description lines changed in each file. The five
gateway tests that read the manifests pass.

agent: pcc-readmodels (c255d7dc)
LamaSu added a commit that referenced this pull request Oct 3, 2026
Merges PR #362 (fix(gateway): operator reads stop fabricating earnings
and fleet data) into #389's branch (feat/readmodels-operator-work).

Conflict: packages/gateway/src/routes/agent-introspection.ts
  - #389 (HEAD) inserted two new AGENT_OPERATIONS entries,
    `operator.work` and `operator.income`, right after the
    `operator.maintenance` entry and before `operator.emergencyStop`.
  - #362 rewrote the `summary` text of the four existing entries
    (operator.machines, operator.earnings, operator.certifications,
    operator.maintenance) to say "Not available yet (501)" plus where
    the real data now lives, since those routes have no backing store
    and were answering fabricated data.
  - The insertion point for #389's two new entries sits immediately
    after one of the four lines #362 rewrote (operator.maintenance),
    which is why git couldn't auto-merge this hunk.
  - Resolution: kept all six entries — #362's four rewritten
    "Not available yet (501)" summaries unchanged, followed by #389's
    two new operator.work / operator.income entries unchanged. No
    overlap in meaning: #362 is marking old fabricated endpoints as
    gone, #389 is adding new real ones.

Conflict: packages/gateway/src/routes/context-pack.ts
  - Same shape: #389 inserted two new "Operator Management" table rows
    (/api/operator/work, /api/operator/income) right after the
    /api/operator/maintenance row and before /api/operator/approvals.
  - #362 rewrote the same four existing rows' descriptions to
    "Not available yet (501): ...".
  - Resolution: kept #362's four rewritten rows followed by #389's two
    new rows, same reasoning as above.

packages/gateway/src/routes/operator.ts, agent-package-test.json and
apps/dashboard/public/agent-package.json merged automatically (no
conflict markers) and were left as git resolved them.

agent: pcc-readmodels (c255d7dc)
LamaSu added a commit that referenced this pull request Oct 3, 2026
The PR steward asked for #389 to be merged up with #362 (bus #5831). This
restacks #409 onto that head.

agent: pcc-readmodels (c255d7dc)
LamaSu added a commit that referenced this pull request Oct 3, 2026
The PR steward asked for #389 to be merged up with #362 (bus #5831). This
restacks #515 onto that head.

agent: pcc-readmodels (c255d7dc)
@LamaSu
LamaSu marked this pull request as ready for review October 3, 2026 22:12
@LamaSu
LamaSu merged commit 670b3b8 into master Oct 3, 2026
8 checks passed
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