Repository navigation
Document read-only threat boundaries per feature; add safe public service mode (#157, #221) - #324
Merged
BigDella merged 3 commits intoOct 21, 2026
Conversation
The committed registry on main was missing duplicate-transaction-scanner, multisig-analyzer, offer-inspector and sponsorship-planner, so CI's "generated registry has no uncommitted changes" step fails. Output of `npm run registry`; no hand edits. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every slice README now carries a "## Threat boundary" section (data sources, outbound requests, proof it cannot sign or submit). verify:features fails a missing/empty/TODO section, with a shrinking migration ledger for the 70 pre-existing slices, and fails any runtime source that calls a signing or submission API. trustline-checker is the reference example. Adds FEATURE_CONTRACT rule 15 with a reviewer checklist, ADR 0007 and a SECURITY.md section. Refs Parcel-Protocol#157 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…enance Adds an operator-declared, app-wide service mode on top of the Parcel-Protocol#184 status feed: a validated public message (secrets, tokens, credential URLs, private addresses and stack frames are refused), an audit-first store restricted to maintainer/system actors, clock-based expiry with a background worker job, GET /api/status, a site banner, and a paused kill switch on the tool route. The 1.0.0 feed schema is unchanged. Configured with UTIX_SERVICE_* env vars; runbook and failure modes in docs/PUBLIC_STATUS.md. Refs Parcel-Protocol#221 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
@Jeyrio Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
closes #157
closes #221
Summary
Two independent changes that both make Utix's safety promises checkable instead of assumed.
## Threat boundarysection with three answers: Data sources, Outbound requests and Cannot sign or submit.verify:featuresnow enforces it. A migration ledger covers the existing slices, and an automated scan fails any slice whose runtime code calls a signing or submission API.trustline-checkeris the documented reference example.degraded,pausedormaintenancewith a short public message. The mode is validated against secret leakage, audited, expires on its own, and is published consistently in three places: the UI (site banner, and a paused kill switch on every tool), the API (GET /api/status) and a background worker (expiry job). It builds on the existing Utix: Add schema-backed public status page data feed #184 status feed without changing that feed's1.0.0schema.A third, mechanical commit regenerates the stale committed registry (details below).
#157: Threat boundaries
What changed
scripts/verify-features.mjsevaluateThreatBoundaryDoc(slug, readme): the section must exist, and each of the three bold-labelled fields needs a real answer on the same line. An empty orTODOanswer fails.findSigningCalls(sources): scans runtime sources forKeypair.fromSecret/fromRawEd25519Seed/random,.sign(…)/signDecorated,signTransaction/signAuthEntry/signMessage/signBlob,submitTransaction/submitAsyncTransactionand the RPCsendTransaction. Comments are stripped first.__tests__/,fixtures/,e2e/andmsw/are skipped, because fixtures legitimately sign sample envelopes from fixed seeds.verifySlicereportsthreatBoundaryPending/threatBoundaryMissing, and the summary line counts pending docs.scripts/threat-boundary-migration.json: ledger of the 70 slices that predate the rule. They warn instead of failing.scripts/new-feature.mjs: the scaffold README now includes the section withTODOplaceholders, so a new slice fails until it is filled in.features/trustline-checker/README.md: the worked example. A new schema test backs up its claim that a pastedS…secret in either address field is rejected before any request.docs/FEATURE_CONTRACT.md: rule 15, with the section format, a reviewer checklist and what the verifier enforces. Also an extra "done" checklist item and the layout note.docs/adr/0007-documented-read-only-threat-boundaries.mdand the ADR index.CONTRIBUTING.mdrequires an ADR for contract changes, so it is marked Proposed for maintainers to accept.SECURITY.md: a "Read-only threat boundaries" section explaining how to review a slice against its boundary.Design decisions
lib/andmsw/handlers.ts.#221: Public service mode
Model (
core/status/service-mode.ts)ServiceModeStateis trusted state. It hasmode, a publicmessage,sinceanduntil, plus internal-onlyreasonandchangedBy.toPublicServiceNoticecopies only the five public fields.validatePublicServiceNoticeenforces that exact schema (1.0.0).checkServiceMessagerefuses an operator message, rather than scrubbing it, if it contains any of these:secret=,token:orapi_key=style pair;localhost;effectiveServiceModeisnormaloutside thesince–untilwindow, so a forgotten mode lifts itself.applyServiceModeraises public components (degraded →degraded, maintenance →under_maintenance, paused →major_outage) and never lowers them. The feed's overall status therefore agrees with the notice, and a declared mode can't hide an observed outage.createServiceModeStore():maintainerandsystemactors may change the mode; ausergetsservice_mode_forbidden;core/admin-configpattern: every attempt is recorded asservice_mode.changed, and if the trail rejects it nothing changes;expire()recordsservice_mode.expired.registerServiceModeJobs(framework, store)registers thestatus.expire_service_modeworker job. That job is the background path.Configuration and API (
core/status/service-config.ts,app/api/status/route.ts)UTIX_SERVICE_MODE,UTIX_SERVICE_MESSAGE,UTIX_SERVICE_SINCEandUTIX_SERVICE_UNTIL. These are server-side and deliberately notNEXT_PUBLIC_*.degraded, notnormal, so an operator's typo stays visible;GET /api/statusreturns{ notice, feed }. It isforce-dynamicand sendsCache-Control: no-store. If either part fails its schema, it answers503 { error: "status_unavailable" }rather than publishing anything partial.UI
ServiceStatusBannersits under the header inAppShell. It uses the existingStatusMessageprimitive and shows the message (or generic copy fromcore/status/copy.ts), the expected end time and a link to/api/status.paused,app/tools/[slug]/page.tsxrendersPausedToolNoticeinstead of mounting the panel, so no tool can issue a request.app/layout.tsxsetsrevalidate = 60, so the statically rendered pages pick up asince/untilboundary within about a minute.Audit
service_mode.changedandservice_mode.expiredare added toSENSITIVE_ACTIONS, and their emitters are registered inscripts/verify-audit.mjs.Design decisions and tradeoffs
1.0.0feed would break consumers that validate exact keys, whichvalidatePublicStatusFeeditself does. The notice has its own versioned schema, and the mode is reflected in the feed through component statuses only.UNTILso a mode can end without a deploy.pausedmaps tomajor_outagein the feed. From a user's point of view, deliberately unavailable tools are an outage. Feed consumers that only know the existing statuses still get a truthful signal.Operational docs
docs/PUBLIC_STATUS.mdcovers:docs/DEPLOYMENT.md,.env.exampleanddocs/ARCHITECTURE.mdare updated.Tests
New and updated tests, all passing:
core/__tests__/threatBoundaryDocs.test.tsMath.sign, non-runtime dirs.verifySlicewith and without the ledger. Every real slice is either documented or in the ledger, the ledger has no stale or unknown slugs, and no real slice has signing calls.features/trustline-checker/__tests__/schema.test.tscore/status/__tests__/service-mode.test.tsapplyServiceModeraise-only behaviour, store authorization, audit-first rejection, denied-attempt auditing, telemetry, the expiry job through a realcreateWorkerFramework.core/status/__tests__/service-config.test.tsbuildStatusResponse200 response per mode with headers, internal fields never in the body, 503 path, telemetry codes.core/status/__tests__/ServiceStatusBanner.test.tsxnormal; title, tone, fallback copy, end time and status link; internal fields never rendered; axe-clean in degraded and paused states;PausedToolNotice.Verification
Unit tests:
npx vitest runpasses for every area this PR touches:core/status,core/audit,core/admin-config,core/ledger,core/contract,core/workers,core/__tests__,core/ui,features/trustline-checkerand the bundle tests. Onecore/admin-configaudit test fails identically onmain(see below).Lint: clean for every file in this PR.
verify:features,verify:boundaries,verify:audit,verify:issues: no new failures compared withmain.Typecheck:
tsc --noEmitreports no new errors (same locations and codes asmain).next build: compiles successfully, then stops at Next's type-check step on a pre-existing error in an untouched file (core/admin-config/store.ts:23,Rolenot exported).mainfails the same way, because it already has 271tscerrors.Pre-existing failures on
main, not touched hereThese reproduce identically on
da96089and are out of scope:tsc: 271 errors.eslint .: 36 errors in files this PR does not touch.verify:features: 6 slices fail structural rules (activity-timeline,duplicate-transaction-scanner,explorer-links,soroban-auth-inspector,soroban-fee-estimator,soroban-spec-viewer).verify:audit: 11 declared actions with no registered emitter.__tests__/core-modules.test.ts,core/ui/__tests__/*,core/admin-config/__tests__/audit.test.ts,core/analytics,core/sessions,core/webhooksandfeatures/testnet-faucet. They were run onmainand on this branch with the same command and produced identical failure lists, test for test.Files changed
#157
scripts/verify-features.mjs,scripts/verify-features.d.mts,scripts/threat-boundary-migration.json(new),scripts/new-feature.mjsfeatures/trustline-checker/README.md,features/trustline-checker/__tests__/schema.test.tsdocs/FEATURE_CONTRACT.md,docs/adr/0007-documented-read-only-threat-boundaries.md(new),docs/adr/README.md,SECURITY.mdcore/__tests__/threatBoundaryDocs.test.ts(new)#221
core/status/service-mode.ts,service-config.ts,ServiceStatusBanner.tsx,copy.ts(all new);core/status/feed.ts(adds the exportedworseComponentStatushelper);core/status/index.tscore/status/__tests__/service-mode.test.ts,service-config.test.ts,ServiceStatusBanner.test.tsx(new)app/api/status/route.ts(new),app/layout.tsx,app/tools/[slug]/page.tsx,core/layout/AppShell.tsxcore/audit/audit.ts,scripts/verify-audit.mjsdocs/PUBLIC_STATUS.md,docs/DEPLOYMENT.md,docs/ARCHITECTURE.md,.env.exampleChore
core/registry/*.generated.ts: output ofnpm run registry.main's committed copies were missingduplicate-transaction-scanner,multisig-analyzer,offer-inspectorandsponsorship-planner, which fails CI's "generated registry has no uncommitted changes" step.Migration and config
1.0.0. The notice is a new endpoint with its own1.0.0schema.UTIX_SERVICE_MODE,UTIX_SERVICE_MESSAGE,UTIX_SERVICE_SINCE,UTIX_SERVICE_UNTIL. When unset the mode isnormaland behaviour is unchanged.## Threat boundarysection. Existing slices are on the ledger and only warn.Deployment notes
normal./api/statusis a new dynamic route. Pages now revalidate every 60 s instead of being fully static, so expect a small increase in render invocations on hosts that bill for them.UTIX_SERVICE_MODE=paused, a public message and preferablyUTIX_SERVICE_UNTIL, then redeploy. Confirm withcurl -s https://<host>/api/status | jq .notice. To roll back, unset the variables and redeploy. The full runbook is indocs/PUBLIC_STATUS.md./api/status. The existing feed module and its consumers are unaffected.Notes for reviewers
dev, but this repository has nodevbranch, so this PR targetsmain.🤖 Generated with Claude Code