AgnCred Threat Model (Closed Pilot)
Status: living document for the production pilot (PILOT_SPEC section 12). Scope: the AgnCred pilot deployment — one same-origin Fastify service serving the API and the built React portal, backed by Postgres. Every claim below cites the implementing file; where a mitigation does not exist, the residual-risk paragraph says so instead of inventing one.
1. System overview and trust boundaries
AgnCred issues portable, cryptographically verifiable work receipts for AI agents. An operator registers agents and issues scoped credentials; the agent runtime submits receipts; a company reviewer attests them with the company's Ed25519 signing key; third parties verify signatures, key history, and the hash-chained event ledger offline via a verification bundle.
Actors
| Actor | Authentication | Authority source |
|---|---|---|
| Operator | Magic-link email → cookie session | Active operator membership (apps/api/src/auth.ts) |
| Agent runtime | Scoped bearer credential (agk_...) |
Credential row bound to one agent (apps/api/src/agent-routes.ts) |
| Company reviewer | Magic-link email → cookie session | Active company membership with owner/admin/reviewer role (apps/api/src/auth.ts) |
| Third-party verifier | None (public endpoints) | Public allowlist projections only (packages/policy/src/index.ts) |
| AgnCred admin | Same session mechanism, agncred_admin role |
Role check in satisfiesRole (apps/api/src/auth.ts) |
Assets
- Company Ed25519 signing keys (private material encrypted at rest,
apps/api/src/key-encryption.ts) - The key-encryption key (KEK),
SIGNING_KEY_ENCRYPTION_KEYenv var - Browser session cookies and CSRF tokens
- Agent API credentials (secret shown once, SHA-256 hash stored)
- Receipt payloads (canonical immutable versions) and the hash-chained event ledger
- Private evidence references (
receipt_private_evidence, erasable) - Reviewer private notes (
receipt_attestations.reviewer_private_note, erasable) - Invitation and magic-link tokens (hashed at rest)
- Personal data (names, emails) in erasable side tables
Trust boundary diagram
TRUST BOUNDARY (network / auth)
..........................................................
: :
+----------------+ : +------------------------------------------------+ :
| Operator |--:-->| Fastify API (same-origin, /api/v1) | :
| (browser, | : | helmet CSP / rate limits / body limit / CORS | :
| cookie+CSRF) | : | [apps/api/src/app.ts] | :
+----------------+ : | | :
: | +------------------+ +---------------------+ | :
+----------------+ : | | Auth: magic link | | Agent routes: | | :
| Agent runtime |--:-->| | cookie sessions, | | scoped agk_ keys, | | :
| (agk_ bearer | : | | CSRF, membership | | idempotency | | :
| credential) | : | | [auth.ts] | | [agent-routes.ts] | | :
+----------------+ : | +------------------+ +---------------------+ | :
: | | :
+----------------+ : | +-------------------+ +---------------------+ | :
| Company |--:-->| | Attest / dispute /| | Invitations | | :
| reviewer | : | | rotate / erase | | (hashed tokens, | | :
| (browser, | : | | [tenant-routes.ts]| | abuse limits) | | :
| cookie+CSRF) | : | +-------------------+ | [invitation- | | :
+----------------+ : | | | routes.ts] | | :
: | v +---------------------+ | :
+----------------+ : | +--------------------------------------+ | :
| AgnCred admin |--:-->| | Signing: Ed25519 attestations, | | :
| (agncred_admin | : | | AES-256-GCM key wrap via KEK | | :
| role) | : | | [key-encryption.ts, core/crypto.ts] | | :
+----------------+ : | +--------------------------------------+ | :
: +-----------------------|------------------------+ :
: v :
: +------------------------------------------------+ :
: | Postgres | :
: | immutable: work_receipt_versions, | :
: | receipt_events (unique seq), attestations | :
: | erasable: private evidence, reviewer notes, | :
: | emails, invitations, sessions | :
: | [db/migrations/003_immutable_proof.sql] | :
: +------------------------------------------------+ :
:..........................................................
|
| PUBLIC BOUNDARY (allowlist projections only)
v
+----------------------------------------------------------------------+
| Third-party verifier: /receipts/:id, /verify, /verification-bundle, |
| /companies/:id/keys — public DTOs built field-by-field |
| [packages/policy/src/index.ts, apps/api/src/verification.ts] |
+----------------------------------------------------------------------+
2. Threats
2.1 Fake companies and reviewers
Attack. An attacker registers a company (self-serve onboarding, or by claiming an invitation they arranged to receive) and "attests" work for an agent they control, giving the agent a fabricated track record.
Impact. Fraudulent reputation on public profiles; erosion of trust in the network.
Current mitigations.
- Companies start
unverifiedand unverified attestations carry zero public score (packages/reputation/src/index.ts: unverified companies contribute zero; tier ceilings). - Domain verification requires a DNS TXT record (
_agncred.<domain>with a server-issued token) checked server-side (apps/api/src/tenant-routes.ts,POST /company/domain-verification/verify). - Registry verification is an explicit admin action (
POST /admin/companies/:id/registry-verify,agncred_adminonly,apps/api/src/tenant-routes.ts) and yields a higher score ceiling than domain verification (packages/reputation/src/index.ts). - Reviewer authority derives from active company membership rows, never from the request body
(
apps/api/src/auth.ts,resolveAuthUser/satisfiesRole); viewers cannot attest. - The public projection labels every receipt with the company's verification tier
(
packages/policy/src/index.ts,buildPublicReceiptProjection).
Residual risk & pilot posture. An attacker who controls a real domain can reach
domain_verified and earn capped points from a company they own. Registry verification is a
manual admin judgment with no automated corporate-registry integration. The domain regex in
tenant-routes.ts (/^[a-z0-9.-]+\.[a-z]{2,}$/) does not perform punycode normalisation or
homograph checks, so a look-alike IDN domain can be verified as-is. Acceptable for a closed
pilot with a small, manually admitted cohort; registry automation and domain hardening are
post-pilot work.
2.2 Related-party attestation
Attack. The operator and the reviewing company are the same party (or colluding), so the "independent" attestation is self-dealing.
Impact. Inflated scores that misrepresent independent validation.
Current mitigations.
- Independence is tri-state (
verified_independent/related/unknown); related and unknown both contribute zero points (packages/reputation/src/index.ts). - The reviewer's independence declaration is part of the signed
approved_public_fields, so it is cryptographically bound to the attestation (apps/api/src/tenant-routes.ts, attest handler;packages/core/src/crypto.ts,attestationSigningPayload). - Admin-maintained linked-party signals override the declaration: a linked pair is scored as
related regardless of what the reviewer claims (
packages/policy/src/index.ts,arePartiesIndependent;packages/reputation/src/index.ts,resolveIndependence). - The reviewer queue shows the independence flag to the reviewer before decision
(
apps/api/src/tenant-routes.ts,/company/queue).
Residual risk & pilot posture. Linked-party signals are populated manually; there is no automated detection (shared payment data, IP overlap, corporate registry cross-checks). A colluding pair that declares "independent" and is not flagged earns points. During the pilot this is bounded by manual admission of participants and diminishing returns per company; a falsely declared independence is at least a signed, non-repudiable statement by the company.
2.3 Stolen sessions
Attack. An attacker obtains a reviewer's or operator's browser session (cookie theft, device theft, session fixation, CSRF).
Impact. Full workspace authority: attest receipts with the company key, invite reviewers, read private evidence, trigger erasure.
Current mitigations.
- Sessions are HttpOnly, Secure (in production), SameSite=Lax cookies — never localStorage
(
apps/api/src/auth.ts,setSessionCookies; fixes baseline defect D4). - Session tokens are 32 random bytes, stored only as SHA-256 hashes; lifetime 24 h with
rotation via
POST /auth/refresh(old session revoked) and revocation on logout (apps/api/src/auth.ts). - State-changing cookie requests require the
x-csrf-tokenheader matching a token derived from the session token; a cross-site attacker can send the cookie but cannot read either cookie to produce the header (apps/api/src/auth.ts,csrfTokenFor,requireUser). - Strict CSP (
script-src 'self',frame-ancestors 'none') reduces XSS-based theft (apps/api/src/app.ts, helmet registration); production refuses wildcard CORS and serves the portal same-origin (apps/api/src/app.ts). - Expired sessions and login tokens are purged by the cleanup command
(
apps/api/src/repository.ts,cleanupExpired).
Residual risk & pilot posture. No device binding, IP pinning, or anomaly detection on
sessions; a stolen cookie is fully usable for up to 24 h. Magic-link tokens travel in URL query
strings (/portal/callback?token=...) and can persist in mail-client logs or browser history;
this is bounded by the 15-minute TTL and single-use consumption
(apps/api/src/auth.ts, consumeLoginToken). Accepted for the pilot; step-up confirmation for
key rotation and erasure is a candidate hardening item.
2.4 Stolen agent keys
Attack. An agent credential (agk_...) leaks from the agent runtime, CI logs, or an env
file.
Impact. The attacker can submit fabricated receipts as that agent and read its receipts and profile — but nothing else.
Current mitigations.
- Credentials are high-entropy (24 random bytes), shown exactly once, stored only as SHA-256
hashes with a visible prefix for identification (
apps/api/src/agent-routes.ts,generateAgentKey, credential creation handler). - Scoped (
receipts:create,receipts:read,profile:read), bound to exactly one agent, optionally expiring, revocable by the operator, and audited on every use (lastUsedAttouch) (apps/api/src/agent-routes.ts,requireAgentCredential). - Agent credentials can never attest, name reviewers, or carry scores: the submission schema
rejects unknown fields (
additionalProperties: false, and Ajv is configured withremoveAdditional: falseso forged fields are rejected, not silently stripped —apps/api/src/app.ts). - Every submitted receipt records the submitting
credential_key_idin the immutable payload and event chain, so post-incident forensics can identify everything a stolen key produced (apps/api/src/agent-routes.ts, payload construction and event metadata). - Fabricated receipts carry zero public weight until a company attests them
(
packages/reputation/src/index.ts). - Per-credential rate limiting (120/min) on agent routes (
apps/api/src/agent-routes.ts,AGENT_RATE_LIMIT).
Residual risk & pilot posture. A stolen key can spam plausible-looking pending receipts and
trigger attestation-request noise until the operator notices (via last_used_at or receipt
listings) and revokes. There is no automatic anomaly detection or key-leak scanning. The
per-credential rate-limit key is the raw Authorization header, so an unauthenticated
brute-forcer gets a fresh bucket per guessed key (see gap list); key entropy (192 bits) makes
brute force infeasible regardless.
2.5 Replay attacks
Attack. Re-submitting a captured request: a magic-link token, an invitation link, an attestation, or a receipt submission.
Impact. Duplicate authority grants or duplicate records.
Current mitigations.
- Magic-link tokens are single-use (
consumeLoginTokenmarks used-at atomically) and expire in 15 minutes (apps/api/src/auth.ts). - Invitation tokens are single-use (claim/accept mark the invitation
accepted), expire after 7 days, and a resend rotates the token so the old link stops working (apps/api/src/invitation-routes.ts). - Receipt submissions are idempotent: a replayed request with the same
Idempotency-Key(or body field) returns the original receipt withreplayed: trueinstead of creating a second record (apps/api/src/agent-routes.ts). - An attestation signature binds
receipt_id,receipt_version,payload_hash,company_id, decision, approved fields, andattested_at, so it cannot be replayed onto a different receipt or version (packages/core/src/crypto.ts,attestationSigningPayload). - Lifecycle transitions are enforced server-side with typed 409s
(
apps/api/src/tenant-routes.ts,assertTransition), so re-posting an accept on an already-accepted receipt fails.
Residual risk & pilot posture. Idempotency keys are scoped per credential; two different credentials replaying the same payload create two receipts (see 2.6). Session cookies replayed within their 24 h lifetime succeed by design (see 2.3).
2.6 Duplicate submissions
Attack. The same piece of work submitted repeatedly (retry storms, or deliberately, to farm multiple attestations from one deliverable).
Impact. Ledger noise; if a reviewer accepts duplicates, inflated scores.
Current mitigations.
- Idempotency-key deduplication per credential (
apps/api/src/agent-routes.ts,findReceiptIdByIdempotencyKey/recordIdempotencyKey; PILOT_SPEC 12 requires unique idempotency constraints at the database). - Operator-path submissions with an existing
receipt_idare rejected with 409receipt_already_exists(apps/api/src/app.ts,POST /receipts). - Each duplicate still requires an explicit human attestation to earn points, and per-company
diminishing returns cap what repeated receipts from one company can add
(
packages/reputation/src/index.ts).
Residual risk & pilot posture. There is no content-based duplicate detection: the same work described twice with different receipt IDs and idempotency keys produces two receipts, and a careless reviewer can accept both. Reviewer diligence plus diminishing returns is the pilot control; similarity flagging is out of scope (no fraud ML, PILOT_SPEC section 3).
2.7 Event chain forks
Attack. Insert, reorder, or branch lifecycle events — e.g. produce two competing histories in which a receipt is both "accepted" and "revoked".
Impact. Ambiguous or falsified lifecycle status; broken auditability.
Current mitigations.
- Events carry per-receipt
sequence_numberandprevious_event_hash; the event hash covers the canonical event including its chain link, so any edit, reorder, or fork breaks every subsequent hash (packages/core/src/crypto.ts,computeEventHashV2,verifyEventChainV2). unique (receipt_id, sequence_number)makes a forked history a database constraint violation, not a data state (apps/api/db/migrations/003_immutable_proof.sql, receipt_events table).receipt_eventshas BEFORE UPDATE/DELETE triggers that reject all mutation (003_immutable_proof.sql,agncred_reject_mutation).- Every verification response and offline bundle re-validates the full chain
(
apps/api/src/verification.ts;apps/api/src/agent-routes.ts,/verification-bundle).
Residual risk & pilot posture. The chain is per-receipt and server-anchored: a database superuser who can drop triggers can rewrite an entire chain self-consistently (see 2.8). There is no external anchoring (public transparency log, periodic checkpoint publication) in the pilot. Third parties who saved an earlier verification bundle can detect after-the-fact rewrites by comparing hashes.
2.8 Database tampering
Attack. Someone with database access (compromised credentials, malicious insider, hosting compromise) edits receipts, attestations, events, or scores directly.
Impact. Falsified history and reputation at the storage layer, below application controls.
Current mitigations.
- Append-only triggers reject UPDATE/DELETE on
work_receipt_versionsandreceipt_events;receipt_attestationspermits only nullingreviewer_private_note(GDPR); receipt index identity columns are locked, andcompany_idmay be set once but never changed (apps/api/db/migrations/003_immutable_proof.sql). - Tampering with a signed payload is detectable: the attestation signature covers the version's
payload hash, and verification recomputes it against the recorded key
(
apps/api/src/verification.ts;packages/core/src/crypto.ts). - Tampering with events breaks the hash chain (2.7).
- Private signing keys are AES-256-GCM ciphertext; the KEK lives in the environment and never
touches the database, so a database-only compromise cannot sign anything
(
apps/api/src/key-encryption.ts). - Scores are not stored authority: they are recomputed deterministically from receipts under a
versioned policy (
packages/reputation/src/index.ts), so editing a score cell has no lasting effect.
Residual risk & pilot posture. Triggers restrain application-role mistakes and casual tampering, but a superuser can disable them. Signature and chain verification make tampering detectable, not preventable — and only for externally held copies once bundles have been downloaded. The pilot accepts single-database trust with standard hosting access controls; external anchoring is future work.
2.9 Signing key compromise (including KEK compromise)
Attack. (a) A company's decrypted private key leaks from process memory or a bug; (b) the
KEK (SIGNING_KEY_ENCRYPTION_KEY) leaks together with a database dump, exposing every
company's private key.
Impact. The attacker can forge attestations that verify as the compromised company (or, for the KEK, any company) until keys are rotated/revoked.
Current mitigations.
- Private keys exist in plaintext only transiently inside the attest handler; at rest they are
AES-256-GCM encrypted with stored nonce and auth tag (
apps/api/src/key-encryption.ts;apps/api/src/tenant-routes.ts, attest handler). Fixes baseline defect D2. - Production refuses to boot without a valid 32-byte KEK; there is no fallback or default
(
apps/api/src/key-encryption.ts,loadKek;apps/api/src/app.tsconstructor check). - Keys are versioned with a recorded
key_idin every signature; rotation activates a new key while old receipts verify against their recorded key forever (apps/api/src/tenant-routes.ts,/company/keys/rotate;003_immutable_proof.sql, one-active-key partial unique index;apps/api/src/verification.tsverifies against the recorded key). - Key status (
active/retired/revoked) is public and included in every verification response and in the key-history endpoints (apps/api/src/verification.ts;apps/api/src/app.ts,/companies/:id/keys), so verifiers can see that a signature was made with a since-revoked key. - Private key material is never logged or exported; tenant export returns public key history
only (
apps/api/src/tenant-routes.ts,/me/export). - Erasure and revocation destroy private material (columns nulled) while retaining public keys
and status permanently (
apps/api/src/repository.ts,eraseCompanyTenant;003_immutable_proof.sql, material-completeness check).
Residual risk & pilot posture. The KEK is a single environment variable — no KMS/HSM, no
KEK rotation or re-wrap procedure, no split knowledge. A host compromise that reads both the
env and the database defeats encryption at rest entirely. Signatures made before a revocation
timestamp are indistinguishable from forgeries made during the compromise window; verifiers see
key_status: revoked and must apply their own policy. Signing happens in the API process, so
an application-level RCE can sign at will. Key rotation is available to any company member with
a review role (owner/admin/reviewer), not owners only. Accepted for the pilot; KMS-backed key
storage and company-held keys are documented as future paths only (PILOT_SPEC section 3).
2.10 Score gaming
Attack. Maximise public score without genuinely validated work: self-attestation, attestation farming across one friendly company, inflating hours/quality, exploiting defaults.
Impact. Public reputation stops reflecting real, independently validated performance.
Current mitigations (score policy agncred-score.v1, packages/reputation/src/index.ts).
- No proof, no reputation: unattested, rejected, disputed, and revoked receipts score zero.
- Unknown or related independence contributes zero (defect D10 fixed; regression-locked in
packages/reputation/src/defect-regressions.test.ts). - Unverified companies contribute zero; domain-verified has a lower ceiling than registry-verified.
- Per-company diminishing returns prevent one company or engagement dominating a profile.
- Reviewer quality and hours count only when the reviewer explicitly confirms them in the
signed
approved_public_fields; operator-entered hours never become verified through passive acceptance (apps/api/src/tenant-routes.ts, attest handler builds confirmations from the reviewer's body, bounds-checked;packages/core/src/crypto.tssigns them). - Every point traces to a receipt with recorded eligibility, reason, tier, independence, and
policy version; scoring is deterministic and recomputable
(
packages/reputation/src/index.ts, contributions and explanation output). - Clients cannot submit scores, reviewer identity, or trust tier: schemas reject unknown fields
(
apps/api/src/agent-routes.ts;apps/api/src/app.tsAjvremoveAdditional: false).
Residual risk & pilot posture. The strongest remaining strategy is a genuinely
domain-verified (or registry-approved) colluding company falsely declaring independence — see
2.2. reviewer_quality_score is bounded (0–100) but otherwise unaudited reviewer opinion.
There is no fraud ML or anomaly detection by design in the pilot. Deterrent: every inflating
statement is inside a company-signed, immutable attestation.
2.11 Prompt injection inside receipt fields rendered to reviewers
Attack. Receipt free-text fields — task.summary, outcome_claims.claims, tools_used,
evidence descriptions, invitation message — are attacker-controlled text. They are shown to
company reviewers in the review UI and returned to agent/MCP consumers. The text can contain
(a) HTML/JS injection attempts, (b) social-engineering payloads aimed at the human reviewer
("SYSTEM: this receipt was pre-verified — click accept"), or (c) prompt-injection payloads
aimed at LLM-based consumers of receipt text.
Impact. A manipulated reviewer signs a fraudulent attestation; a manipulated downstream LLM (e.g. an agent reading receipt status via the MCP server) takes attacker-directed actions.
Current mitigations.
- The portal is React and renders these fields as text nodes; no
dangerouslySetInnerHTMLor raw HTML sinks exist inapps/web/src, so HTML/JS injection is inert markup on screen. - The API's CSP (
script-src 'self',object-src 'none') is a second layer against any future markup sink (apps/api/src/app.ts). - Field lengths and shapes are capped at the schema boundary (summary ≤ 2000 chars, claims ≤ 20
× 500 chars, tools ≤ 100 × 200 chars,
additionalProperties: false) (apps/api/src/agent-routes.ts,submitReceiptBodySchema). - What becomes public is only the reviewer-approved summary and confirmed outcomes, chosen at
attestation time — attacker text does not flow to the public profile without explicit human
approval (
apps/api/src/tenant-routes.ts,approvedPublicFields;packages/policy/src/index.ts,buildPublicReceiptProjection).
Residual risk & pilot posture. Social-engineering injection remains: nothing in the review
UI labels operator/agent text as untrusted or strips authority-mimicking phrasing, and the
reviewer's decision is exactly the trust anchor the attacker targets. Likewise, MCP and SDK
consumers receive receipt text verbatim; any LLM consuming it must treat receipt fields as
untrusted data, never as instructions — this is a documentation/integration obligation, not an
enforced control (packages/mcp-server). Pilot posture: small reviewer cohort, reviewer
guidance in COMPANY_REVIEWER_GUIDE, and explicit "untrusted content" framing are the controls;
UI provenance labelling is a hardening candidate.
2.12 Public data leakage
Attack. Private evidence, reviewer notes, exact timestamps, emails, or internal references reach a public endpoint through serialization drift or an over-broad query.
Impact. Confidentiality breach toward the very companies whose trust the network depends on; GDPR exposure.
Current mitigations.
- Public output is built by explicit allowlist DTOs — every public field is deliberately
copied, nothing is spread or passed through, so a new private field cannot leak by default
(
packages/policy/src/index.ts:buildPublicReceiptProjection,buildPublicAgentProfile,buildPublicCompanyProfile,buildPublicEventProjection). Replaces the deprecated blacklist redaction (defect D9; the old functions are retained demo-only and marked deprecated in the same file). - Public receipts expose only approved fields: coarse completion month (
completed_onis YYYY-MM), reviewer-approved summary/outcomes, decision, signature metadata, lifecycle status, score contribution (packages/policy/src/index.ts). - The public event projection carries chain facts only; event metadata never crosses that
boundary (
buildPublicEventProjection). - Private evidence and reviewer notes live in separate tables/columns reachable only through
authenticated, tenancy-checked routes (
apps/api/src/tenant-routes.ts,/receipts/:id/private-evidenceenforces operator-or-reviewing-company). - Company name visibility is a per-receipt choice (
hidden/verified_badge/public) honoured in the projection (packages/policy/src/index.ts).
Residual risk & pilot posture. One boundary is looser than the rest: the public
/receipts/:id/verification-bundle returns stored events including metadata (needed because
event hashes cover metadata), and event metadata includes free-text dispute reasons and
pseudonymous actor/user IDs (apps/api/src/agent-routes.ts, verification-bundle handler;
apps/api/src/tenant-routes.ts, dispute handler stores reason in metadata). Whatever a
disputer types becomes effectively public and immutable. This is listed as a known gap.
Snapshot tests asserting private-field absence are the regression control (PILOT_SPEC 11).
2.13 Email abuse (invitation spam, enumeration)
Attack. (a) Use AgnCred's mail identity to spam or phish arbitrary addresses via invitations with attacker-authored messages; (b) enumerate which emails hold accounts.
Impact. Deliverability/reputation damage, phishing on AgnCred letterhead, privacy leak of membership.
Current mitigations.
- Invitations: max 20 per operator per rolling 24 h, 5-minute resend cooldown, 7-day expiry,
per-route rate limits, tokens hashed at rest, resend rotates the token
(
apps/api/src/invitation-routes.ts,MAX_INVITATIONS_PER_DAY,RESEND_COOLDOWN_MS). POST /auth/magic-linkanswers identically whether or not the account exists ("sent"), so it cannot enumerate accounts (apps/api/src/auth.ts).- Token-minting and mail-sending endpoints carry tighter per-IP limits (10/min auth, 20/min
session) on top of the global 300/min limit (
apps/api/src/auth.ts,AUTH_RATE_LIMIT;apps/api/src/app.ts). - Production refuses to expose login tokens in responses and fails closed without a real mail
provider (
apps/api/src/app.tsboot checks; defect D6). - Expired invitations have their email and message purged by retention cleanup
(
apps/api/src/repository.ts,cleanupExpired). - Onboarding does not disclose whether an email already exists (non-enumerating; see below).
Onboarding endpoints are non-enumerating: an already-registered email receives an
indistinguishable { status: "check_email" } response (with a sign-in link for the real
account) instead of a 409, so onboarding and /auth/magic-link both refuse to confirm
whether an account exists (apps/api/src/auth.ts).
Residual risk & pilot posture. The invitation email embeds the operator-authored message
verbatim (apps/api/src/invitation-routes.ts, sendInvitationMail), so within the 20/day cap
an operator can send attacker-phrased text under AgnCred's sender. Acceptable in a closed pilot
where operators are admitted manually and can be suspended.
2.14 Erasure versus proof retention (GDPR tension)
Attack / tension. Not an attacker in the classic sense: a data subject exercises erasure (GDPR Art. 17), which naively conflicts with the promise that signed receipts stay verifiable forever. Conversely, an attacker could try to use erasure as a weapon to destroy inconvenient proof.
Impact. Either unlawful retention of personal data, or destruction of the network's cryptographic history.
Current mitigations (design: immutable pseudonymous proof + erasable side tables).
- Immutable proof tables (
work_receipt_versions,receipt_events,receipt_attestations) hold pseudonymous IDs (opr_*,agt_*,usr_*,org_*) and approved metadata only; names, emails, private references and reviewer notes live in erasable tables/columns (apps/api/db/migrations/003_immutable_proof.sql; PILOT_SPEC 5.4). - Operator erasure pseudonymises operator and agent records, deletes private evidence, strips
invitation emails/messages, revokes credentials, and stamps
privacy_erased_at— without touching signed payloads or events (apps/api/src/repository.ts,eraseOperatorTenant). - Company erasure pseudonymises the company, nulls reviewer private notes (the only mutation
the attestation trigger permits), deletes domain verifications, and destroys private key
material while retaining public keys and status with reason
gdpr_erasure(apps/api/src/repository.ts,eraseCompanyTenant;003_immutable_proof.sql,agncred_attestation_guard). - Erasure is an owner-only action derived from membership, requires explicit confirmation, and
sends a confirmation email (
apps/api/src/tenant-routes.ts,/me/delete). - Verification survives erasure: signatures verify against retained public keys, and the report
discloses
privacy_erased: trueas a separate fact (apps/api/src/verification.ts). - Weaponised erasure fails: erasure never rewrites payloads, events, or attestations, so it cannot destroy proof of past decisions.
Residual risk & pilot posture. Canonical receipt payloads are immutable by design; if an operator typed direct personal data into free-text fields (task summary, claims), it cannot be removed from the stored version — the guidance "never store personal data in receipt payloads" is procedural, not enforced by a scanner. Dispute reasons in event metadata are similarly unerasable (see 2.12). Pseudonymous IDs remain personal data under GDPR if any mapping survives; the pilot's legal review (docs/LEGAL_REVIEW_REQUIRED.md) must confirm the retained set qualifies as sufficiently de-identified or is retained under a lawful basis.
3. Out of scope for the pilot
Deliberately excluded from the pilot build (PILOT_SPEC sections 3 and 12); listing them here so their absence is a documented decision, not an oversight:
- Registry verification is manual.
registry_verifiedis granted by an admin endpoint after human review of evidence (apps/api/src/tenant-routes.ts); there is no automated corporate-registry integration. - No anomaly/fraud ML. No behavioural scoring, duplicate-content detection, collusion graphs, or automated related-party discovery. Controls are deterministic policy rules plus manual admin flags and suspensions.
- Company-held keys are documented-only. All signing keys are generated and used server-side under the KEK; a path where companies hold their own keys is documented as future work, not implemented.
- No outbound webhooks. Integrations poll for status (
GET /agent/receipts/:id); there is no outbound callback surface, which also removes SSRF and callback-forgery exposure for the pilot.
Also out of scope: marketplace, payments, agent hosting, open (non-invited) reviews, and legal contract generation (PILOT_SPEC section 3).