97 Commits

Author SHA1 Message Date
Claude eb385707fa feat(observability): OpenTelemetry tracing + log/trace correlation
CI / build (push) Successful in 3m49s
Deploy iios-service / build-deploy (push) Successful in 4m19s
Adds OTel auto-instrumentation (http, express, prisma/pg, redis, ...) exporting
OTLP to Tempo, so iios-service requests appear as distributed traces in Grafana.

observability/tracing.ts is imported FIRST in main.ts — auto-instrumentation
patches modules as they are require()d, so anything loaded earlier is never
traced. Env-driven (OTEL_SERVICE_NAME / OTEL_EXPORTER_OTLP_ENDPOINT); with the
endpoint unset the SDK never starts, so dev and tests carry zero overhead.
Health/metrics probes are excluded — they would otherwise swamp Tempo and bury
real request traces.

The existing P9 trace middleware now adopts the ACTIVE OTel trace id, so an
http.request log line and its distributed trace share one id (Loki -> Tempo
pivot in Grafana), falling back to x-trace-id / traceparent / generated.

pnpm-lock.yaml regenerated — CI and the Dockerfile both use
'pnpm install --frozen-lockfile', which would hard-fail on an unchanged lock.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 13:14:53 +00:00
maaz519 0a8544b6fc feat(iios): notification engine — presence-gated Web Push (engine phase)
Deploy iios-service / build-deploy (push) Failing after 4m37s
CI / build (push) Successful in 5m25s
Generic notification engine: reacts to message.sent and runs three gates before
delivering — policy (DM always; group only on @mention or reply-to-you), presence
(skip if the recipient is focused on that thread), mute (per-thread). Delivery via a
swappable NotificationPort (Web Push/VAPID adapter); a 'gone' (404/410) prunes the sub.

- PresenceService + gateway `focus_thread` signal + disconnect cleanup (room membership
  != viewing, since the sidebar joins every thread room).
- IiosNotificationSubscription table + `muted` on IiosThreadParticipant (migration).
- Endpoints: GET vapid-public-key, POST/DELETE subscribe, POST threads/:id/mute|unmute;
  listThreads returns the caller's `muted`.
- DM-vs-group read from the opaque `membership` attribute in the notification POLICY only
  — kernel stays generic (grep-verified).

Tests: 13 new (3 gates + reply-to-you + prune + web-push adapter states); full suite 205
green. Verified live: module boots, subscribe stored, mute/unmute round-trips.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 19:16:53 +05:30
maaz519 24a87f6fb6 feat(iios): media sharing — presigned storage port + attachments on messages
Deploy iios-service / build-deploy (push) Failing after 3m35s
CI / build (push) Successful in 3m47s
Media the industry way: the DB stores a reference, bytes live behind a storage port.
- StoragePort + LocalDiskStorage (dev). MediaService presigns short-lived signed
  upload/download URLs (HS256 tokens) pointing at IIOS's own endpoints; the bytes
  never touch the kernel. Prod swaps STORAGE_PORT to S3/Supabase — same as auth.
- MediaController: presign-upload / PUT upload/:token (raw stream) / GET blob/:token /
  presign-download. Uploads are OPA-governed (iios.media.upload: 25 MB cap +
  image/video/audio/pdf/office allowlist); downloads are tenant-fenced by object key.
- send() + MessageDto carry an attachment (contentRef/mimeType/sizeBytes → generic
  MEDIA_REF/VOICE_REF/FILE_REF part; DTO exposes kind image|video|audio|file).

Tests: media.service.spec (6) — round-trip, oversize/type denied, oversized PUT
refused, tampered token rejected, tenant fence. Full suite 192 green. Verified live:
presign→upload→send→history→signed download round-trips the exact bytes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-09 01:15:03 +05:30
maaz519 1256664361 feat(iios): expose senderId (stable externalId) on MessageDto
CI / build (push) Successful in 4m53s
Deploy iios-service / build-deploy (push) Failing after 8m37s
The app needs a reliable "is this message mine?" signal. senderActorId worked only
with a lazily-learned actorId, which Supabase token-refresh wipes. senderId is the
sender's externalId (email/username) — always present and comparable to the session
user — so message alignment is deterministic. Verified: senderId round-trips = email.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:58:10 +05:30
maaz519 0ecc8a4ada feat(iios): multi-issuer session verifier (many IdPs/apps → isolated appId scopes)
SessionVerifier now holds a REGISTRY of trusted OIDC issuers instead of a single
Supabase project. A token is routed by its `iss` claim to that issuer's entry,
verified against that issuer's JWKS (ES256, no secret), and stamped with that
entry's appId/orgId — so two Supabase projects / IdPs map to two isolated app
scopes on one IIOS (chat vs a future support app). App A's tokens can't reach B.

- Config: `AUTH_ISSUERS` (JSON array of { url, appId, orgId? }); the single
  `SUPABASE_URL` (+ SUPABASE_APP_ID) still works as a one-entry shorthand.
- Per-issuer JWKS cache; untrusted issuer → reject; forgery (right issuer claim,
  wrong key) → reject.
- HS256 app-token path (dev/tests) unchanged.

Tests: new session.verifier.spec (4) — routes to correct appId, second issuer →
different appId, untrusted issuer rejected, cross-issuer forgery rejected.

Note: outbox `replay.spec` has a pre-existing clock-skew flake (nextAttemptAt vs
Postgres now()) that surfaces under certain suite orderings — unrelated to this
change (fails in isolation on a clean tree; passes when another spec runs first).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:38:30 +05:30
maaz519 e956ad3cb9 feat(iios): verify real Supabase access tokens (ES256 via JWKS)
SessionVerifier gains a Supabase mode (set SUPABASE_URL): user SATs are ES256,
verified against the project's public JWKS — no shared secret. Keys are fetched at
startup (onModuleInit) and cached as PEM so verify() stays synchronous; a rotated
kid triggers a background refresh. Claims map to MessagePrincipal with userId=email
(stable, human-readable → mentions/directory keep working; RealMDM canonicalises
later). The legacy HS256 app-token path is unchanged (dev/tests untouched).

senderName now prefers the actor display name over the raw handle, so real names
show on bubbles when identity is an email.

Verified end-to-end against a live project: signup → real ES256 SAT → GET /v1/threads
200 → thread created + owned by the resolved email principal. Full suite 182 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:14:28 +05:30
maaz519 8c814d9b86 feat(iios): @mentions via inbox fan-out + my-annotations query (pins/saves)
Mentions ride the existing event-driven inbox — no chat parsing in the kernel:
- send() carries an OPAQUE mentions[] (userIds) into the message event; the kernel
  never parses "@". The app supplies the notify-list.
- InboxProjector fans out a MENTION inbox item (new generic inbox kind) to each
  mentioned *participant* (never the sender), idempotent per source message; reading
  the thread resolves the reader's MENTION + NEEDS_REPLY items to DONE.
- New MENTION value in the generic IiosInboxItemKind taxonomy (migration).

Pins/saves reuse the annotation primitive; new generic query powers a Saved list:
- MessageService.listMyAnnotated(principal, type) + GET /v1/threads/my-annotations
  returns the caller's annotated messages (type "save") with thread context.

Tests: 182 pass (+4: mention fan-out, resolve-on-read, saved query, opaque mentions).
Verified live: mention→inbox delivery, read→resolve, pin/save persistence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 01:35:30 +05:30
maaz519 f3c4ba72b5 feat(iios): generic interaction-annotation primitive (backs emoji reactions)
Adds IiosInteractionAnnotation — an actor attaches an OPAQUE (annotationType, value)
label to an interaction. The kernel stores + aggregates them but never interprets the
strings (the chat app writes type "reaction" / value = emoji); the same primitive backs
pins/saves/flags/tags later. No chat vocabulary in kernel code — verified by grep.

- MessageService.toggleAnnotation(): governed (participant-only via new OPA rule
  iios.interaction.annotate), idempotent toggle keyed on
  (scope, interaction, actor, type, value); history DTO carries aggregated
  { type, value, users[] } groups.
- Gateway: `annotate` event → broadcasts `annotation` (refreshed user list) to the room.
- DevOpaPort: annotate allowed only for thread members (real OPA can tighten later).
- Migration add_interaction_annotations (additive; dev data untouched).

Tests: 178 pass (+3: toggle/aggregate, coexisting values, non-member denied).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 00:20:47 +05:30
maaz519 c4206f9809 fix(iios): thread subject on create, graceful open_thread error, isolated test DB
- openThread/createThread accept a generic `subject` (group name) — stored on the
  thread, echoed via listThreads; kernel never interprets it.
- Gateway open_thread now returns an ACK'd { error } on failure instead of throwing
  (which never acked → clients hung on "loading"). Non-member/missing-thread opens
  fail cleanly.
- Tests run against an isolated `iios_test` database (vitest globalSetup creates +
  migrates it; test.env overrides DATABASE_URL) so `pnpm test` can never TRUNCATE the
  dev database again. Verified: full suite green, dev DB row counts unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 19:39:19 +05:30
maaz519 1af10d0f4a feat(iios): message senderName + dev user directory
- Messages now carry senderName (resolved actorId → sourceHandle.externalId) so
  clients render usernames instead of raw actor cuids.
- GET /v1/dev/users exposes the dev directory (DEV_USERS keys) so a host app can
  validate add-by-username; real apps validate against their user store / MDM.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 17:11:20 +05:30
maaz519 31f7682a46 feat(iios): include participant names in listThreads (generic)
GET /v1/threads now returns participants[] (member usernames) so a host app can
render conversation titles / member lists without a second call. Still generic —
it only lists members of threads the caller belongs to.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:27:47 +05:30
maaz519 ba745bb71a feat(iios): policy-enforced membership + threaded replies + dev IdP (generic)
Enriches the platform PLANE, not the kernel:
- DevOpaPort: the dev OPA stub now evaluates a small policy table (behind the
  same opa.decide) — DM capped at 2, add-participant requires member/group-admin,
  governed self-join for membership threads. Real OPA swaps in unchanged.
- Dev IdP login (POST /v1/dev/login) issues the same JWT claims a real IdP would.
- PolicyDeniedFilter maps fail-closed denials to HTTP 403.

Generic kernel additions (no chat vocabulary — 'dm'/'group' live only as OPA
policy + an opaque thread attribute):
- MessageService.addParticipant (governed membership by userId), governed
  openThread self-join (scoped to threads with a membership attribute),
  parentInteractionId on send (reply link), and a generic listThreads.
- REST: GET /v1/threads, POST /v1/threads, POST /v1/threads/:id/participants;
  socket add_participant + membership/parentInteractionId. ensureParticipant
  gains a role.

Tests: dev-opa.port.spec + message.spec (DM cap / group admin / governed join /
listThreads / reply). smoke-membership.mjs; realtime smokes updated for governed
join. 175 unit tests + all smokes green; kernel free of dm/group literals.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:27:47 +05:30
maaz519 f0ea1ca553 feat(iios): email adapter dedup fix + smoke + verification
CI / build (push) Failing after 2m8s
Adds adapter-owned externalEventId extraction to the ChannelAdapter contract
(WebhookAdapter→eventId, EmailAdapter→Message-ID) and uses it in InboundService,
so email replays dedup on Message-ID (previously only payload.eventId worked).
Adds an email-dedup spec + smoke-email.mjs proving inbound → normalized, a reply
joins the same email thread, bad signature rejected, and outbound EMAIL SENT.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:34:26 +05:30
maaz519 f0afca89e3 feat(iios): register EmailAdapter for EMAIL + email-shaped egress provider
AdapterRegistry now backs the EMAIL channel with the email-native EmailAdapter
(RFC threading) instead of the generic WebhookAdapter. Adds EmailProvider — an
email-envelope egress provider ({to,subject,text,html,inReplyTo}) registered for
EMAIL when IIOS_PROVIDER_URL_EMAIL is set (sandbox stays the default). Spec:
signed email → normalized interaction on the email thread; a reply joins the
same thread (one conversation); bad signature → rejected (fail-closed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:28:35 +05:30
maaz519 ae50f56404 feat(iios): EmailAdapter (email-native normalize + threading) in adapter-sdk
Adds a canonical EmailInboundPayload + EmailAdapter implementing the
ChannelAdapter contract: HMAC signature verify + normalize with email-native
threading (whole reply chain → one IIOS thread via references/inReplyTo root),
messageId as providerEventId for dedup, html→text fallback, Re:/Fwd: stripping.
Plus emailInboundFixture/emailReplyFixture and unit tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 20:25:00 +05:30
maaz519 427396653f feat(iios): socket.io Redis adapter for cross-replica realtime (P9 scaling)
Adds an opt-in RedisIoAdapter (wired in main.ts when REDIS_URL is set) so
socket.io room emits fan out across instances via Redis pub/sub — a client on
replica B now receives messages emitted by replica A. Without REDIS_URL the
in-memory adapter is kept (single-instance dev unchanged). Adds redis to
docker-compose, REDIS_URL to the env contract, and smoke-realtime-cluster.mjs
which proves cross-instance delivery fails without Redis and passes with it.
Delivery is at-least-once at N>1; clients dedupe by message id (documented).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:28:11 +05:30
maaz519 a520b26398 chore(iios): production Dockerfile + migrate entrypoint + env/secrets contract
Adds a multi-stage Dockerfile for iios-service (build → pnpm deploy prune →
slim non-root runtime), a docker-entrypoint that runs `prisma migrate deploy`
then starts the server as PID 1, a .dockerignore, a fully-commented
.env.example config contract, and docs/DEPLOYMENT.md (topology, scaling,
release strategy, prod-readiness gaps). Moves prisma to dependencies so the
CLI ships in the prod bundle. Image builds, migrates, and serves /health 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:19:13 +05:30
maaz519 a933e2e3af feat(iios): idempotency ledger on outbound egress (409 on key reuse) (P9)
OutboundService.send is now opt-in idempotent: an explicit idempotencyKey routes
the send through IdempotencyService.run (Slice 6), so reusing a key with a
different target/payload returns a 409 conflict and a same-key/same-request retry
replays the cached command. The inner findUnique early-return stays as a backstop
so a FAILED-then-retried send never collides on the command's unique key. Keyless
sends keep the old behavior (no ledger row).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 18:44:22 +05:30
maaz519 cdba0f0179 feat(iios): retention sweep dev endpoint + /metrics + smoke (P9)
POST /v1/dev/retention/sweep runs the sweep on demand; /metrics gains a global
retention section (snapshot counts by status). smoke-retention.mjs (0-day
windows) proves an aged interaction is redacted in place and surfaces as a
REDACTED snapshot on /metrics.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 12:57:51 +05:30
maaz519 035315ef73 feat(iios): retention_policy_snapshot + sweep (archive→redact, hold-aware) (P9)
Adds IiosRetentionPolicySnapshot (per-interaction frozen lifecycle timestamps)
+ IiosInteraction.dataClass, and a RetentionService whose sweep captures
snapshots then archives (status change) and, past delete_after, redacts content
in place (reusing the Slice-8 tombstone) — never a hard delete. An active
compliance hold blocks both; every action is audited and tenant-scopeable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 12:50:49 +05:30
maaz519 a5cd8f0ec4 feat(iios): /v1/dsr erasure + compliance-hold REST + smoke (P9)
Tenant-scoped /v1/dsr: POST /erase (self-service right-to-erasure), POST/GET
/holds + POST /holds/:id/release. smoke-dsr.mjs proves PII present → hold
blocks erase (409) → release → erase redacts the ticket subject in place →
re-erase idempotent → cross-tenant cannot see the hold.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 06:27:42 +05:30
maaz519 611ca74cea feat(iios): DSR right-to-erasure (redact-in-place) + compliance hold (P9)
Adds IiosComplianceHold + IiosSourceHandle.redactedAt and a DsrService whose
eraseSubject redacts the caller's PII in place (message bodies, identity
display, raw payloads, support/inbox/transcript text) while preserving
externalId (source preservation), structure, and the audit trail. An active
compliance hold blocks erasure until released; every op is audited and
tenant-fenced. Erasure is idempotent (KG-03).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 03:24:43 +05:30
maaz519 67da42a103 feat(iios): projection-cursor observability on /metrics + smoke (P9)
Adds a global `projections` section to GET /metrics exposing each cursor's
position (last_event_id/last_offset), determinism checksum, and lagMs.
smoke-projection.mjs injects events and asserts the projection's ordered
high-water-mark advances with a 64-hex rolling checksum (KG-06).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:48:14 +05:30
maaz519 2424739e3a feat(iios): projectors checkpoint the projection cursor (P9)
All 5 projectors (route/ai/inbox/calendar/raw-event) now advance their
IiosProjectionCursor after a freshly-claimed event is applied — split into
claim → apply → advance so a duplicate never double-counts and a throw leaves
the cursor untouched (event replays via the DLQ). Inbox's two topics get two
cursor rows. Proves KG-06 replay reproduction at the projection level.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:44:23 +05:30
maaz519 1997fe48bb feat(iios): projection-cursor ledger + ProjectionCursorService (P9)
Adds the doc-mandated IiosProjectionCursor (ordered high-water-mark +
rolling checksum per projection/topic/partition) and a reusable
ProjectionCursorService.advance(). The checksum is a deterministic fold over
the ordered event stream, so replaying the same events reproduces it — the
KG-06 proof that replay reproduces projection outcomes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:39:31 +05:30
maaz519 cdb399a9c1 feat(iios): idempotency smoke + P9 slice-6 verification
smoke-idempotency.mjs: dup POST /v1/support/tickets with the same
Idempotency-Key replays the same ticket, same key + different body → 409,
and a second tenant reusing the key gets its own ticket (isolation).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:27:20 +05:30
maaz519 093f6484b4 feat(iios): idempotent support ticket + callback creates via ledger (P9)
SupportService.createTicket + requestCallback now accept an optional
idempotencyKey (from the Idempotency-Key header). With a key, a same-body
retry replays the same row and a different-body reuse is a 409; without one,
they run unwrapped as before. Tenant-scoped via the command ledger.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:22:19 +05:30
maaz519 b8b7e0d3e8 feat(iios): idempotency-command ledger + IdempotencyService (P9)
Adds the doc-mandated IiosIdempotencyCommand table + a reusable
IdempotencyService.run() that writes IN_FLIGHT at command start, replays the
cached response on a same-key/same-body retry, rejects a same-key/different-body
reuse (409), and retries a FAILED command. Tenant-scoped via
@@unique([scopeId, commandName, idempotencyKey]) (KG-02/KG-06).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:19:03 +05:30
maaz519 dbf8f814c0 feat(iios): tenant-scoped /v1/dlq REST + chaos endpoint + smoke (P9 slice 5)
GET /v1/dlq lists the caller tenant's dead-letters; POST /v1/dlq/:id/replay
fences by assertOwns (KG-02) + audits each replay. Dev-only POST
/v1/dev/chaos/poison + ChaosConsumer (fails first delivery, succeeds on
replay) drive smoke-dlq.mjs: poison → QUARANTINED → cross-tenant denied →
owner replay → RESOLVED. Adds dev.chaos.v1 contract event.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 02:01:40 +05:30
maaz519 d3d557766e feat(iios): wire projectors + relay into DLQ (P9 chaos-safe)
Projectors register a replay handler and route bus errors to
dlq.onConsumerFailure (dead-letter + clear the claim-then-fail claim)
instead of swallowing them. Relay dead-letters an outbox event after
IIOS_OUTBOX_MAX_ATTEMPTS instead of retrying forever. Chaos test proves
poison → DLQ → replay resolves while sibling events keep flowing (KG-13/KG-06).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:43:57 +05:30
maaz519 208fb0b4f0 feat(p9): IiosDlqItem + DlqService (dead-letter + deterministic replay)
Task D.1: mandated dlq_item table (sourceType/sourceId/failureStage/errorCode/
retryCount/payloadRef/consumerName/scopeId/status). DlqService.record (idempotent
per consumer+sourceId, bumps retryCount), onConsumerFailure (quarantine + clear the
IiosProcessedEvent claim so it's replayable — fixes claim-then-fail), registerHandler
+ replay (clears claim, awaits the registered consumer handler → RESOLVED on success,
QUARANTINED+retry on repeat throw; outbox-sourced items reset to PENDING). Wired into
OutboxModule; resetDb truncates it. 3 tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:28:48 +05:30
maaz519 e5d3ce5ecd feat(p9): tenant-scoped /metrics + observability smoke; slice-4 verification green
Task O.4: GET /metrics (session-auth) returns the caller tenant's counters
(interactions by kind/status, outbound by status, AI jobs + cost, tickets,
meetings, inbox, audit-link count) via findScope + Prisma groupBy — no cross-tenant
totals — plus a global relay-lag section. smoke-observability proves the mandated
gate end to end: x-trace-id response header, event correlationId == request trace,
audit link in DB, /metrics counters. Slice-4 verification: 122 tests green, builds,
boundary clean, observability smoke + all prior smokes pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:14:20 +05:30
maaz519 6548b40f17 feat(p9): iios_audit_link table + audit binding at decision points
Task O.3: IiosAuditLink (traceId/correlationId/scopeId/actorRefId/action/
resourceType/resourceId) + recordAudit() helper (reads the current trace from ALS,
best-effort). Written at the decision/mutation points — route approve/deny, AI
accept/reject, meeting summarize, outbound send — so the trace appears in DB and a
decision can be replayed / traced across services (mandated iios_audit_link).
Migration audit-link; resetDb truncates it. 2 tests bind approve/accept to a trace.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:08:55 +05:30
maaz519 1d3e18f417 feat(p9): propagate trace id into emitted events + structured relay/gateway logs
Task O.2: ai/calendar/route/ingest emit sites now stamp traceparent + insignia.
correlationId from interaction.traceId ?? currentTraceId() ?? generated (mirroring
message.send; inbox/inbound already carried correlationId). toTraceparent is hex-safe
(UUID used as-is, other ids hashed). OutboxRelay logs a structured relay.publish per
event (eventType/aggregateId/correlationId); MessageGateway logs ws.deliver. Tests:
an event's correlationId equals the active request trace, with a generated fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 01:04:24 +05:30
maaz519 0f923303d3 feat(p9): request-scoped trace context + middleware + structured logger
Task O.1: AsyncLocalStorage trace context (currentTraceId/runWithTrace/toTraceparent)
read by singleton services with no DI churn; Express traceMiddleware derives a trace
id per request (x-trace-id → traceparent → generated), echoes x-trace-id on the
response, runs the request in-context, and logs a structured http.request line on
finish. Minimal JSON structured logger stamps traceId on every line. Wired via
app.use in main.ts. 4 tests: scope, concurrency isolation, traceparent shape/parse.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 00:50:15 +05:30
maaz519 ed53decd3e feat(p9): tenant isolation smoke + slice-3 verification green
Task T3.5: smoke-tenant.mjs — two dev tokens (org_A/org_B); tenant B reading A's
meeting → 403 (KG-02), B's list excludes A's, A reads its own → 200. Slice-3
verification: 114 tests green, pnpm -r build all packages, boundary clean, tenant
smoke + capability smoke + all prior smokes (realtime/inbox/support/adapter/route/
ai/calendar) pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:59:51 +05:30
maaz519 e7b8845c8a feat(p9): per-tenant outbound quota + cellId assignment
Task T3.4: OutboundService gains a per-tenant egress cap (tenantLimit/tenantWindowMs,
IIOS_TENANT_OUTBOUND_*) counting a scope's commands in the window — a noisy tenant
is RATE_LIMITED while others keep sending (noisy-neighbor protection). Persists
scopeId on every command (fixes the previously-unscoped table). resolveScope assigns
IiosScope.cellId (IIOS_CELL_ID, default cell-default) — the cell-partition hook.
Tests: per-tenant cap isolates tenants; new scopes carry a cellId.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:55:14 +05:30
maaz519 d002c8f521 test(p9): cross-tenant isolation suite (KG-10)
Task T3.3: isolation.spec.ts — two tenants (org_A/org_B) on the same app. Proves
tenant B is denied on approve/getArtifact/accept/getMeeting/summarize of A's
resources, B's lists (decisions/artifacts/meetings) show none of A's, the denied
calls neither mutate A's data nor create a scope for B, and each tenant sees only
its own lists. The mandated one-DB tenant-isolation test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:53:03 +05:30
maaz519 ed0d4dae80 feat(p9): fence AI + calendar + message by-id leaks (tenant scope)
Task T3.2: AiJobService.getArtifact/accept/reject assertOwns on artifact.job.scopeId;
listArtifacts scoped to caller. CalendarService.getMeeting/setConsent/generateTranscript/
summarize/listActionItems assertOwns on meeting.scopeId (optional principal → asserts
when supplied by a controller, skipped for internal calls). MessageService.getMessageById
gains the same optional fence. Controllers thread the principal through. All 46
ai/calendar/routing/messaging tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:51:10 +05:30
maaz519 063049b724 feat(p9): scope-ownership assert + fence routing approve/deny/listDecisions
Task T3.1: ActorResolver gains findScope (no-create) + assertOwns(principal,
resourceScopeId) → ForbiddenException on mismatch (KG-02). RouteService.approve/
deny now assertOwns on the decision's binding scope before mutating; listDecisions
is scoped to the caller. Fixes the critical cross-tenant mutation (tenant A could
approve tenant B's route). Also adds IiosScope.cellId + IiosOutboundCommand.scopeId
columns (migration tenant-isolation) + cell assignment on scope create. Test:
another tenant approving → Forbidden, decision unchanged, no scope created, empty list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:46:40 +05:30
maaz519 27103529e6 feat(p9): consent-at-egress smoke + slice-2 verification green
Task S2.3: capability smoke asserts the CMP consent gate is consulted at egress
(NOT_REQUIRED → send proceeds); the DENY→BLOCKED and ALLOW→receipt paths are
unit-covered. Slice-2 verification: 110 tests green, pnpm -r build all packages,
boundary clean, capability smoke PASS, all prior smokes (realtime/inbox/support/
adapter/route/ai/calendar) still pass through the consent-gated broker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:33:51 +05:30
maaz519 cdc59be481 feat(p9): thread purpose + persist consent receipt through OutboundService
Task S2.2: OutboundService.send gains an optional purpose, passes it to the broker,
and persists the returned consentReceiptRef on IiosOutboundCommand (new nullable
column, migration outbound-consent). RouteService.execute passes 'route_forward';
calendar reminders pass 'meeting_reminder'. CMP-DENY → command BLOCKED, no send.
2 tests: receipt recorded on allow, purpose-DENY blocks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:26:01 +05:30
maaz519 2223d7a891 feat(p9): CMP consent gate in the Capability Broker
Task S2.1: the broker now checks consent (cmp.checkPurpose) after the OPA gate +
obligations and before the provider — a DENY blocks the send (BLOCKED/CONSENT_DENIED,
provider never called); ALLOW/NOT_REQUIRED proceed, ALLOW carrying the consent
receipt. Contracts gain CapabilityRequest.purpose + CapabilityResult.consentReceiptRef.
FakeCmp is now purpose-aware (denyPurpose/allowOnly). 5 new tests: allow+receipt,
deny→blocked, not-required, purpose-aware, OPA-before-CMP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 21:23:49 +05:30
maaz519 f7519d36f0 feat(p9): real-egress proof (env-gated HttpProvider) + /health bindings + smoke
Task 9.5: /health now reports the egress provider backing each channel (sandbox
vs http). scripts/smoke-capability.mjs proves the slice end to end: sandbox egress
SENT through the broker; EMAIL routed to the env-gated HttpProvider and actually
delivered to a LOCAL echo server (real egress, no external network, no credentials);
idempotent replay; /health bindings. App wires CapabilityModule.

P9 slice-1 verification: 103 tests green, pnpm -r build all packages+demos,
boundary clean, capability smoke PASS, all prior smokes (realtime/inbox/support/
adapter/route/ai/calendar) still PASS through the broker.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:38:47 +05:30
maaz519 a1f46646ee feat(p9): adapter certification checklist (certifyProvider)
Task 9.4: certifyProvider runs the P9 conformance checklist a provider must pass
before backing a channel — declares explicit capabilities, returns providerRef +
recognized outcome (never throws), repeatable/idempotent, tolerates empty payload.
2 tests: SandboxProvider passes; a throwing/no-capability provider fails with the
specific failing checks listed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:05:04 +05:30
maaz519 28e517fc1b feat(p9): route OutboundService through the Capability Broker
Task 9.3: OutboundService now delegates the execute step to CapabilityBroker
(policy-gated + obligation-enforced) instead of calling a provider directly, while
keeping idempotency + rate-limit + the command/attempt ledger. BLOCKED obligations
→ command BLOCKED; a fail-closed throw marks the command FAILED then propagates.
AdaptersModule imports CapabilityModule; P4/P6/P8 egress now flows through the
broker unchanged. Updated 6 spec constructions; added a DENY_OUTBOUND test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 20:04:11 +05:30
maaz519 c50a501f32 feat(p9): Capability Broker — governed egress + fail-closed gate + obligations
Tasks 9.1+9.2: contracts (CapabilityRequest/Result/Provider, PolicyObligation) +
service CapabilityBroker that fails closed via opa (PolicyDeniedError, nothing sent),
enforces obligations before any provider call (DENY_OUTBOUND/REVIEW → BLOCKED;
MASK/REDACT → mask payload field), then routes to a CapabilityProviderRegistry
(SandboxProvider default; env-gated HttpProvider overrides per channel; unknown
channel fails closed). 5 tests: allow/deny/block/redact/unknown-channel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 19:58:25 +05:30
maaz519 e11964b8bc feat(p8): meeting-studio demo + smoke-calendar; P8 verification green
Task 8.7: apps/meeting-studio (Vite, port 5178) — schedule a meeting, invite
attendees, toggle per-attendee recording consent, generate transcript (BLOCKED
until unanimous consent — KG-14), summarize, see AI summary + action-item inbox.
scripts/smoke-calendar.mjs proves the P8 guarantees end to end: direct schedule,
consent gate BLOCKED→READY (KG-14), summary linked to a P7 ai_artifact, action
items, attendee-visibility exclusion (Scenario 6), meeting_request genesis,
simulated provider sync (idempotent).

P8 verification: 95 tests green, pnpm -r build all packages+demos, boundary
passes and fails on meeting-web->service, smoke-calendar PASS, prior smokes
(realtime/inbox/support/adapter/route/ai) PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:22:24 +05:30
maaz519 ca9498ce83 feat(p8): /v1/calendar REST + kernel-client methods + @insignia/iios-meeting-web
Task 8.6: CalendarController exposes schedule/request(+fromCallback/fromClaim)/
list/get/consent/transcript/summarize/action-items/providers(+sync) (session-auth).
kernel-client RestClient gains scheduleMeeting/requestMeeting/listMeetings/
getMeeting/setConsent/generateTranscript/summarizeMeeting/listActionItems/
connect+syncCalendarProvider + Meeting/MeetingParticipant/MeetingActionItem types.
New @insignia/iios-meeting-web (MeetingProvider + useMeetings/useMeeting/useSchedule/
useConsent/useSummarize/useActionItems). Boundary: meeting-web -> contracts +
kernel-client (fails on meeting-web->service).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:09:18 +05:30
maaz519 83369f2055 feat(p8): simulated calendar adapter (provider · sync cursor · availability)
Task 8.5: CalendarAdapter connects a SIMULATED provider account + sync cursor,
deterministically pulls stable external events (idempotent by externalEventId,
advancing the cursor), records availability windows, and computes free/busy from
overlapping calendar events. No network. Real provider via Capability Broker = P9.
3 tests: connect, sync idempotent + cursor advance, availability busy/free.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 18:02:27 +05:30