Repository navigation
Description
qa#3 · os-tesla · session_01L9qibw6yXcLi9aRRBpPT4W
Slice C-api of qa#3's round 2 part C: the delta 8e3d4a5 → c419609 (the release tip after the drained queue), api/cli surface, 27 of 52 items; C-browser files its own record.
Environment fingerprint
- framework objectstack-ai/objectstack main @ c419609 (
fix(plugin-auth): a directory deactivation revokes the user's API keys, and no one restores themfix(plugin-auth): a directory deactivation revokes the user's API keys, and no one restores them #33075), the 18.4.0 release tip;ui/in the same tree, no Console build (every item is api / cli surface). - Increment over part B's subject 8e3d4a5, five commits: c419609 (fix(plugin-auth): a directory deactivation revokes the user's API keys, and no one restores them #33075 plugin-auth), b232220 (fix(plugin-form): the modal and drawer record forms save a picked permission-set holder's package; pin the create form's seed rules #32931 plugin-form holder package), 9df85e1 (feat(approvals): a submitter's agent can recall and resubmit its own approval request over MCP (#32993) #33076 approvals MCP recall/resubmit), e003ebb (fix(automation): a flow's answer also names the read-only fields its map items and resumed subflow children dropped (#33019) #33068 automation read-only fields in map items and subflows), d60a0de (fix(pm): the leading-sigil near-miss regex refuses a long symbol run in linear time, so the live half-state sweep no longer hangs on a drawn rule #33053 pm tooling, no product surface). 52 files changed.
- Wave: qa#3 seat [PM seat] qa · seat 3 — 🟢 os-tesla #33014, anchor [sweep] v18.4.0 release test sweep #32908, round 2 part C, triage#1 「retest plugin-auth/core/platform-objects items touched by fix(plugin-auth): a directory deactivation revokes the user's API keys, and no one restores them #33075 feat(approvals): a submitter's agent can recall and resubmit its own approval request over MCP (#32993) #33076 fix(plugin-form): the modal and drawer record forms save a picked permission-set holder's package; pin the create form's seed rules #32931 fix(automation): a flow's answer also names the read-only fields its map items and resumed subflow children dropped (#33019) #33068」.
- Build: same tree as part B,
git checkout c4196099,pnpm install --frozen-lockfile(6 s) +OS_SKIP_DTS=1 turbo buildof the showcase, CLI and dogfood closures (70 packages, 1 m 10 s, 0 cached) +env -u OS_SKIP_DTS pnpm --filter @objectstack/spec build(spec with.d.ts). Node 22.23.4, pnpm 10.31.0, 4 CPUs, 15 GiB shared memory cgroup. - App: stock showcase via
objectstack dev --seed-admin -p PORT -d file:/tmp/qa3b/RUNNER/db/ITEM.db --no-compile --no-watchwithOS_PORT=PORT. Disjoint port ranges 4810–4849, one server per runner at a time. Scratch app copies for the CLI codemod, the connector boots (compiled under their own OS_PORT) and anything that writes into the app. The walledisolated/groupboots for the P0 no-active-org item were started by hand with the posture env vars plus--no-compile.stdiolegs ran onos start. The tree'sdist/objectstack.jsonand.objectstack/installed-packages/were untouched (checked). - Seed:
--seed-admin+ demo personas. Runtime personas, keys, SCIM connections and service accounts were minted per item through HTTP doors. - Execution: parallel, 4 runner subagents (opus, effort
high): C1 (2FA, password reset), C2 (keys, SCIM, AI service accounts, P0 no-active-org), C3 (automation, approvals, P0 disable-durable), C4 (MCP incl. P0 stdio, REST, CLI, connectors, import). Pins at--maxWorkers=1, dogfood one file per invocation: no OOM kill this time. - Wall-clock: setup 3 min (10:08–10:11: checkout, install, build, spec dts), done in parallel with drafting the B-api record. Testing 36 min wall (10:11–10:47; per runner C1 14 min, C2 19, C3 22, C4 36, each including 6–7 min of its own reading). Verification 4 min (C1's two security probes re-run on 18.3.0 83d7e4a). Cost at draft time:
get_sessionexternal_metadata.usage.cost_usd= 112.33 for the whole session (B-api + C-api; part B alone was 82.77).
Scope
Selector touched:8e3d4a54 resolved at c419609 by node scripts/checklist-select.mjs touched:8e3d4a54 --json → 52 runnable items. This slice = items-C-api.md, 27 items (8 with pins, 19 manual; 3 P0). Every revision below is the one the resolver reported; all 27 matched the item JSON. Every item that also ran in part B kept its revision (no checklist edit in the increment). No planned item is in this slice. Weighting followed ROUND2-C: identity-auth API-key, SCIM and session items, then approvals and automation. Each increment commit got a probe on the live doors it changes (below).
Per-clause tables: record-C-api-clauses-{C1,C2,C3,C4}.md (21–51 KB each; one comment per file).
Item verdicts
| item | prio | tier | rev | verdict | clauses p/f/b/s | runner |
|---|---|---|---|---|---|---|
| access-security.no-active-org-session-semantics | P0 | T1 | 10 | PARTIAL | 15/0/1/0 (A0 as A0a–c) | C2 |
| access-security.packaged-flow-write-door-parity | P1 | T2 | 4 | PASS | 6/0/0/0 | C3 |
| ai.mcp-stdio-fail-closed | P0 | T1 | 9 | PASS | 13/0/0/0 | C4 |
| ai.mcp-run-action-exposure-gate | P1 | T2 | 7 | PASS | 7/0/0/0 | C4 |
| ai.mcp-oauth-private-host-transport | P1 | T1 | 6 | PASS | 9/0/0/0 | C4 |
| api-backend.route-ledger-live-parity | P2 | T3 | 4 | PASS | 12/0/0/0 | C4 |
| api-backend.api-methods-verb-gate | P1 | T2 | 4 | PASS | 11/0/0/0 | C4 |
| approvals.sla-escalation | P2 | T3 | 4 | PASS | 8/0/0/0 | C3 |
| automation.flow-toggle-kill-switch | P2 | T3 | 4 | PASS | 7/0/0/0 | C3 |
| automation.trigger-status-contract | P1 | T2 | 14 | PASS | 14/0/0/0 | C3 |
| automation.packaged-flow-disable-durable | P0 | T2 | 10 | PASS | 9/0/0/0 | C3 |
| automation.packaged-flow-subflow-disable-refusal | P1 | T2 | 12 | PASS | 7/0/0/0 | C3 |
| automation.flow-run-semantics-v18 | P1 | T1 | 3 | PASS | 5/0/0/0 | C3 |
| cli.migrate-meta-codemod | P1 | T2 | 7 | PASS | 15/0/0/0 | C4 |
| identity-auth.self-service-password-reset | P1 | T2 | 5 | FAIL | 9/1/0/0 (1 = security row) | C1 |
| identity-auth.two-factor-enrollment-reveal | P1 | T2 | 5 | PASS | 9/0/0/0 | C1 |
| identity-auth.two-factor-verify-to-activate | P1 | T2 | 5 | FAIL | 8/1/0/0 (1 = security row) | C1 |
| identity-auth.two-factor-backup-codes | P1 | T2 | 6 | PASS | 10/0/0/0 | C1 |
| identity-auth.two-factor-disable-lifecycle | P1 | T2 | 3 | PASS | 9/0/0/0 (N3 n/a: the fresh-grace branch was the one observed) | C1 |
| identity-auth.api-key-expiry-lifecycle | P1 | T1 | 3 | PASS | 4/0/0/0 | C2 |
| identity-auth.import-invitation-and-sso-domain-answers | P2 | T1 | 3 | PASS | 4/0/0/0 | C2 |
| identity-auth.idp-group-permission-set-mapping | P1 | T1 | 1 | FAIL | 5/1/1/0 (1 = security row; SSO leg blocked) | C2 |
| identity-auth.ai-service-account-lifecycle | P1 | T1 | 1 | PASS | 6/0/0/0 | C2 |
| identity-auth.ai-service-account-register-and-key | P1 | T1 | 1 | PASS | 8/0/0/0 | C2 |
| integration-system.connector-declarative-boot | P1 | T1 | 6 | PASS | 10/0/0/0 | C4 |
| integration-system.connector-degraded-recovery | P1 | T2 | 7 | PASS | 7/0/0/0 | C4 |
| records-forms.named-import-mapping | P2 | T3 | 1 | PASS | 4/0/0/0 | C4 |
Pins at c419609, all green, 0 failed. C2: plugin-security 29, rest 5, runtime 14, plugin-auth 197 over 10 files (including #33075's own scim-deactivation-reconcile-user and api-key-restore-guard), plugin-email 32, core 25, dogfood 40 over 5 files. C3: service-automation 72, runtime 52, verify 16, dogfood flow-door-elevated-start 27, plus corroborating plugin-approvals 18 and dogfood approval-decide-over-mcp 8. C4: mcp 27, plugin-auth 114, spec 47, service-automation 110, dogfood 2. C1's items carry no pin. Each clause was scored at its pin's layer (RUNNER rule 6), with the live leg driven where the pin stops above the oracle.
Clauses that did not pass
| item | clause | verdict | why |
|---|---|---|---|
| identity-auth.self-service-password-reset | probe beside the clauses | fail | security finding — detail withheld, reported to PM |
| identity-auth.two-factor-verify-to-activate | probe beside the clauses | fail | security finding — detail withheld, reported to PM |
| identity-auth.idp-group-permission-set-mapping | probe beside the clauses | fail | security finding — detail withheld, reported to PM |
| identity-auth.idp-group-permission-set-mapping | SSO leg | blocked(fixture) | no stock OIDC identity provider |
| access-security.no-active-org-session-semantics | A0c (group producer (1) read) | blocked(fixture) | knownGaps[0], unchanged |
| identity-auth.two-factor-disable-lifecycle | N3 | n/a (scored as not contradicted) | the fresh-grace branch was the one this run met |
Fails: reproduction rule and disposition
- identity-auth.self-service-password-reset · probe beside the clauses · security finding — detail withheld, reported to PM.
- identity-auth.two-factor-verify-to-activate · probe beside the clauses · security finding — detail withheld, reported to PM.
- identity-auth.idp-group-permission-set-mapping · probe beside the clauses · security finding — detail withheld, reported to PM.
No release blocker. All three P0s hold. ai.mcp-stdio-fail-closed passes 13/13. automation.packaged-flow-disable-durable passes 9/9: after a real cold restart the disabled packaged flow reads enabled:false, bound:false, an urgent task starts no run, and re-enable updates the same ledger row. access-security.no-active-org-session-semantics passes every driven clause on hand-booted walled isolated and group postures. Its A7 on group answers a 403 naming the missing organization, not a 500, and NULL-organization row counts stay 0 around every refused write. One leg stays blocked on its known fixture gap.
Extraction (cards drafted, not filed)
No new product card. No clause failed and no non-security probe defect turned up. Re-measured as still present at c419609 (no new draft):
- card-B-api-2 (Switching admin_full_access off through POST /security/_activation is accepted for the last platform admin, and nothing in the product can switch it back on #33082–checklist: re-point three api clauses that lag the product at 8e3d4a54 (QA 18.4.0 R2 part B, B-api slice) #33086 range, OAuth AS metadata advertises an https issuer on a private plain-HTTP deployment). It reproduces at c419609 on the private boot and also on the public boot.
- card-api-4 (MCP
list_actionslists a deployment-authored action on a packaged object twice, with arefusedFacesentry). - card-api-5 (stdio
get_recordon a hidden row answers uncoded not-found text).
Increment probes (rule 8, on the commits' own doors)
- c419609 / fix(plugin-auth): a directory deactivation revokes the user's API keys, and no one restores them #33075, SCIM deactivation revokes API keys (C2, ~6 min). SCIM
PATCH active:falserevoked all three keys the user owned and stamped eachscim_deactivated: one live, one self-revoked, one admin-revoked, the earlier reasons restamped. Each key answers403 API_KEY_REVOKEDwith the cannot-be-restored sentence, on RESTGET /data/showcase_taskand on MCPtools/call. Another user's key and the admin's key still answer 200. Restore is refused 403 for the owner and for a platform admin, and so is a data-doorPATCH {revoked:false}. The audit log has oneupdaterow per key, actorsvc:scim:<connection>.PATCH active:truelifts the ban, the old keys stay revoked, and a newly minted key works.DELETE /Users/{id}revokes the new key the same way. The commit's rule holds on every door tried. A revoked key answers 403, not 401, by design (coreapi-key.ts); an unknown key gets 401. - 9df85e1 / feat(approvals): a submitter's agent can recall and resubmit its own approval request over MCP (#32993) #33076, MCP recall/resubmit (C3). Over the live MCP endpoint, only the submitter's agent could recall or resubmit, and only after its confirmation step. The approver got FORBIDDEN, a non-participant 404 REQUEST_NOT_FOUND, and an admin could recall a pending request but not resubmit. The request did not move on any refusal.
- e003ebb / fix(automation): a flow's answer also names the read-only fields its map items and resumed subflow children dropped (#33019) #33068, read-only fields named (C3). A map's answer names the read-only field its items dropped, as one merged entry. A paused subflow child names it too, whether resumed by the parent's run id or its own; in the latter case it shows on the parent's subflow step. The path is reachable only past
OS_ALLOW_UNLINTED_METADATA_WRITES=1, because a stock save refuses any flow writing a read-only field. No clause was scored on that boot. - b232220 / fix(plugin-form): the modal and drawer record forms save a picked permission-set holder's package; pin the create form's seed rules #32931, plugin-form holder package: console form surface only, so it is not in this api slice (C-browser).
- Per-item probe lines (1–4 min each) are in the group files. They covered boundary values on every identity door (empty, malformed and unknown emails, ids, tokens, codes), half-signed-in and anonymous callers, walled vs single posture, cross-caller import idempotency, and connector fault arms.
Parked-on #32908 (checklist accuracy and fixture gaps; not extracted)
Stale or wrong item text, no verdict moved:
identity-auth.two-factor-enrollment-revealA4 verify: "a pre-verify bearer answers 401" (QA run · release-18.3.0-w3-manual-api (25/25) · c4c515ad · 2026-10-29 · 23 PASS / 2 PARTIAL / 0 FAIL / 0 BLOCKED / 0 NOT-RUN #27392) does not hold at c419609 on either credential (pre-verify get-totp-uri → 200 or 400). The only 401 is the same token used after verify-totp rotated the session. Re-word: "a token minted before the verify answers 401 after it".identity-auth.self-service-password-resetstep 8: theprevious_password_hashesring holds only passwords written whilepassword_history_count > 0. Also, a PASSWORD_REUSE refusal leaves the token usable.identity-auth.self-service-password-resetfixture:/auth/configreportsfeatures.emailDelivery:falseon stock while reset mail lands insys_identity_emailwith statussent. Don't read it as "no mail".identity-auth.two-factor-disable-lifecyclestep 8 / A5:mfa_requiredhas three levels, and a gated user cannot restore the setting (403 MFA_REQUIRED) until they enroll again. The grace stamp is written within the disable and survives re-enrollment.identity-auth.two-factor-backup-codesfixtures[0]: "one sequence on one boot" isn't needed; each item drove its own enable → verify.access-security.no-active-org-session-semanticssteps[8]: a plain member's showcase_account create meets the generic CRUD 403, and showcase_task now requiresproject. Name showcase_task (with a project) or showcase_private_note as the tenant-scoped targets. A0 mixes legs of different scorability; scored as A0a–A0c.identity-auth.import-invitation-and-sso-domain-answersfixtures.requires[2]: the domain doors needOS_SSO_ENABLED=trueon both boots.sys_identity_emailhas no template-id column.identity-auth.idp-group-permission-set-mappingsteps[3]: with the projected grant present, the hand grant is refused 409 UNIQUE_VIOLATION. Hand-grant first, as the pin's CONTROL does. An unknown set name gets the boundary sentence rather than "no such set".identity-auth.ai-service-account-lifecycleA0/A2 "both reads answer 200": under the agent ceiling a fresh account's key answers 403 (ADR-0151 D3). Name the grant in fixtures.requires; a built-in ceiling set is refused as a person grant (400).- Coverage gap: no checklist item asserts fix(plugin-auth): a directory deactivation revokes the user's API keys, and no one restores them #33075's SCIM-deactivation key revocation live (unit pins only). C2's probe above is a ready recipe: mint credential → POST /Users → admin set-user-password → keys → PATCH active false/true → DELETE. Its revoked-key answer is 403
API_KEY_REVOKED, not 401. content/docs/ai/connect-mcp.mdx(9df85e1): "anyone else is refused (FORBIDDEN)" — a caller who cannot read the request gets 404 REQUEST_NOT_FOUND first. That is the safer behaviour; only the wording is imprecise.- fix(automation): a flow's answer also names the read-only fields its map items and resumed subflow children dropped (#33019) #33068 reachability:
droppedFieldsover HTTP needs a fixture past the lint (OS_ALLOW_UNLINTED_METADATA_WRITES=1or a pre-rule stored flow). A future item should name it. automation.trigger-status-contractA5: only the deregistration arm was driven, as in part B.automation.flow-run-semantics-v18steps[1]:qa_elevatedhas no recipe body (autolaunched, runAs system, create_record was used).api-backend.route-ledger-live-parity: 16 route ledgers, not 11. REST_ROUTE_LEDGER has 20 families.POST /data/:object/actions/:nameis correctly a routing 404; don't sample it as a ledger row.api-backend.api-methods-verb-gateA1 prose: restore/purge follow fromdelete(unchanged since part B).cli.migrate-meta-codemodstep 7: two mode-less decisions, not three.--from 17 --to 19says nothing about 18 → 19 having no step.ai.mcp-run-action-exposure-gate: adminlist_actionsis 47 entries (Mei 35), against 45/33 at 8e3d4a5. The clause allows it.records-forms.named-import-mappingA1: say "idempotent per caller". A member re-importing the admin's file creates duplicates on the private-OWD object because the upsert match sees only the caller's rows. That is RLS working.ai.mcp-oauth-private-host-transportsteps[3] (real client from a second machine) stays blocked(fixture); it scores no clause.
Environment facts for RUNNER:
scripts/qa/qa-fixture.mjs walled-bootrunsos devwithout--no-compile, so on a shared checkout it rewritesexamples/app-showcase/dist/objectstack.jsonunder every other runner. Pass--no-compilethere, and have RUNNER's walled-boot entry say so.cd APP && setsid cmd &records the subshell's pid, sokill -- -PIDmisses the server. Record$!of a baresetsid … &(C4's slip, fixed mid-run; no verdict rests on it).- Run vitest at
--maxWorkers=1with dogfood one file per invocation. With 4 runners this gave no OOM kills, against repeated kills with 8 runners at--maxWorkers=2in part B.
Activity
objectstack-fleet commentedon Nov 13, 2026
qa#3 · os-tesla · session_01L9qibw6yXcLi9aRRBpPT4W
qa#3 · 18.4.0 R2 part C · C-api · group C1 — subject c419609
Runner C1, sequential (one server of mine up at a time). Tree boot
objectstack dev --seed-admin --no-compile --no-watchfromexamples/app-showcase, admin admin@objectos.ai / admin123, one port + one file DB per item: 4810file:/tmp/qa3b/C1/db/identity-auth.self-service-password-reset.db(unarmed boot); 4811…/identity-auth.two-factor-enrollment-reveal.db, 4812…/identity-auth.two-factor-verify-to-activate.db, 4813…/identity-auth.two-factor-backup-codes.db, 4814…/identity-auth.two-factor-disable-lifecycle.db— all four withOS_AUTH_TWO_FACTOR=trueexported BEFORE boot; 4815…/probe-reset-r2.db(one probe re-run boot for the reset item). Each 2FA boot runs its own enable → session-lane verify sequence, so every item's precondition is established on its own boot (the backup-codes fixture's "one boot for the three enrollment items" is satisfied per boot). No scratch app copies. Row reads = read-onlynode:sqliteon the boot's DB file (the fixtures' live-boot equivalent of the system-context read); the one write is the reset item'ssys_verification.expires_atexpiry leg the step prescribes. TOTP = hand-rolled RFC 6238 helper copied frompackages/qa/dogfood/test/totp.ts. No item carriesautomated.ref→ no Tier-1 pins (pin totals 0/0). Driver scripts and raw outputs:/tmp/qa3b/C1/bin/*.mjs,/tmp/qa3b/C1/<item>/run*.txt, server logs/tmp/qa3b/C1/<item>/server.log. Revisions in the area JSON match the dispatch (5 / 5 / 5 / 6 / 3). Headline commit c419609 (#33075) changes onlyreconcileScimUserLifecycle's inactive branch + the API-key restore guard; none of the doors below runs through it, and nothing here moved vs the ee9ad83 round.identity-auth.self-service-password-reset (rev 5) — FAIL (every clause pass; one security row)
pins: none · live: 4810, db identity-auth.self-service-password-reset.db (target user resetee@qa.test created by admin
POST /api/v1/auth/admin/create-user→ 200; it carried mustChangePassword:true, which did not interfere)POST /api/v1/auth/request-password-reset {email:resetee@qa.test, redirectTo:/reset}→ 200{"status":true,"message":"If this email exists in our system, check your email for the reset link"}; adminGET /api/v1/data/sys_identity_email→ row toReset Tee <resetee@qa.test>, subject "Reset your ObjectStack password", status sent, body_text + body_html carryhttp://localhost:4810/api/v1/auth/reset-password/<24-char token>?callbackURL=%2Freset;GET /api/v1/data/sys_email→ 0 rows (identity mail is deployment-level, as the fixture says); the link answers GET 302POST /api/v1/auth/reset-password {token, newPassword}→ 200{"status":true}; sign-in NEW → 200 with token; sign-in OLD → 401 INVALID_EMAIL_OR_PASSWORD; a session opened before the reset → get-session 401 UNAUTHENTICATED (reset ends other sessions){"message":"Invalid token","code":"INVALID_TOKEN"}; NEW still signs in 200, the attempted password 401UPDATE sys_verification SET expires_at = <now-1h ISO> WHERE identifier = 'reset-password:TOKEN'→ 1 changed row (was now+1h); reset → 400 INVALID_TOKEN; current password signs in 200, attempted one 401. Garbage token, last-char-tampered token,""and missing token → each 400 INVALID_TOKEN; current password still 200, attempted 401password_history_countdefaults 0 (GET /api/settings/auth), so it was armed for the step:PUT /api/settings/auth {values:{password_history_count:3}}→ 200; reset to the CURRENT password → 400 PASSWORD_REUSE "For security you can't reuse one of your last 3 passwords…" (token survives the refusal: the same token then reset to another password 200); after that, resetting back to the previous password → 400 PASSWORD_REUSE; restored to 0 → 200. A password set while the count was 0 was not in the ring (accepted) — see Parked.{}/{email:""}/{email:"not-an-email"}→ 400 VALIDATION_ERROR (shape-level, identical for any address, no enumeration); upper-cased known address → 200 same body, mail delivered to the stored lowercase address;GET /reset-password/garbage?callbackURL=/reset→ 302Location: …/reset?error=INVALID_TOKEN;newPassword:"short"→ 400 PASSWORD_TOO_SHORT; spec namePOST /api/v1/auth/forget-password→ 404 (the live/spec divergence step 1 names, still present); target user reading sys_identity_email → 403 PERMISSION_DENIED, anon → 401.identity-auth.two-factor-enrollment-reveal (rev 5) — PASS
pins: none · live: 4811, db identity-auth.two-factor-enrollment-reveal.db, OS_AUTH_TWO_FACTOR=true before boot
GET /api/v1/auth/config→ 200{success:true, data.features.twoFactor:true}; anonPOST /api/v1/auth/two-factor/enable {}→ 400 VALIDATION_ERROR (non-404)POST /two-factor/enable {password:admin123}(cookie session) → 200 keys [method, totpURI, backupCodes];otpauth://totp/labelObjectStack:admin@objectos.ai, params secret/issuer=ObjectStack/digits=6/period=30, algorithm absent (SHA-1 default); secret length 52, base32Decode clean → 32 bytesxxxxx-xxxxx(values not recorded)GET /api/v1/meta/object/sys_two_factor→ enable_two_factor type api target /api/v1/auth/two-factor/enable, resultDialog.fields [{totpURI, qrcode}, {backupCodes, code-list}], acknowledge "I have saved my backup codes"; response.totpURI string, response.backupCodes string[10]Authorization: Bearer, no cookie): correct 200 SAME secret · wrong 400 INVALID_PASSWORD. Session-lane verify-totp → 200 {token, user} + rotatedbetter-auth.session_token; post-verify cookie (rotated): correct 200 SAME secret · wrong 400 · none 400; post-verify echoed-token bearer: correct 200 SAME · wrong 400; the pre-verify token after rotation → 401 (get-session 401). Password-gated in every cell → no carve-outGET /api/v1/meta/object/sys_user→ enable_two_factor resultDialog fields [{totpURI, qrcode}, {backupCodes, code-list}] + acknowledge, successMessage absent; both paths resolve (string / string[10])select count(*) from sys_two_factor where user_id=admin= 0; anon{password}→ 401 UNAUTHORIZED, count 0identity-auth.two-factor-verify-to-activate (rev 5) — FAIL (every clause pass; one security row)
pins: none · live: 4812, db identity-auth.two-factor-verify-to-activate.db, OS_AUTH_TWO_FACTOR=true before boot
{success:true, data.features.twoFactor:true}; anon enable{}→ 400POST /api/v1/auth/sign-in/email→ 200 keys [redirect, token, user, session], no twoFactorRedirect, Set-Cookie onlybetter-auth.session_token(no two_factor cookie); that client's get-session 200 and data read 200; sys_user.two_factor_enabled = 0. TOTP_NOT_ENABLED cited from two-factor-lockout.dogfood.test.ts, not drivenPOST /two-factor/verify-totp {code}with the live session → 200 {token, user} + rotated session cookie; row verified = 1, failed_verification_count 0; sys_user.two_factor_enabled = 1; rotated session get-session 200{"twoFactorRedirect":true,"twoFactorMethods":["totp"]}, Set-Cookie clears session_token/session_data and setsbetter-auth.two_factor; that client's get-session 401 UNAUTHENTICATED andGET /api/v1/data/showcase_task401; cookie-lane verify-totp → 200 {token:string, user} (+ session cookie, two_factor cleared); get-session then 200 admin@objectos.ai, data read 200identity-auth.two-factor-backup-codes (rev 6) — PASS
pins: none · live: 4813, db identity-auth.two-factor-backup-codes.db, OS_AUTH_TWO_FACTOR=true before boot
{success:true, data.features.twoFactor:true}; anon enable{}→ 400; enable 200 (10 codes retained) → session-lane verify-totp 200 → row verified 1, two_factor_enabled 1better-auth.two_factorcookie →POST /two-factor/verify-backup-code {code:B0}through the cookie → 200 {token, user}; get-session 200 admin@objectos.ai, data read 200POST /two-factor/generate-backup-codes {password}→ 200 keys [status, backupCodes], 10 new strings, 0 overlap with the old set; fresh challenges: old UNSPENT B4 → 401 INVALID_BACKUP_CODE, old unspent B9 → 401; new N0 → 200 + usable sessionsecretcolumn is 144 chars and does not contain the base32 secret.)GET /api/v1/meta/object/sys_user→ generate_backup_codes type api target /api/v1/auth/two-factor/generate-backup-codes, refreshAfter false, successMessage absent, resultDialog fields [{backupCodes, code-list}], acknowledge "I have saved the new codes", copy "Previous backup codes are now invalid…"; response.backupCodes = string[10]. (sys_two_factor.regenerate_backup_codes: same dialog, refreshAfter true)"", upper-cased, dash-less and space-padded codes → 401 INVALID_BACKUP_CODE each (no lenient normalisation); verify-backup-code on the SESSION lane (live session, no challenge cookie) → 200 and the code is consumed (the same code at a later challenge → 401) — single-use holds across lanes. No findings.identity-auth.two-factor-disable-lifecycle (rev 3) — PASS
pins: none · live: 4814, db identity-auth.two-factor-disable-lifecycle.db, OS_AUTH_TWO_FACTOR=true before boot · mfa_required leg = Setup patch on the same boot (
PUT /api/settings/auth), restored to off / 7 at the end (read back){success:true, data.features.twoFactor:true}; enable 200 + session-lane verify 200 → rows 1, verified 1, two_factor_enabled 1; BEFORE: fresh sign-in → 200{"twoFactorRedirect":true,"twoFactorMethods":["totp"]}+better-auth.two_factorcookiePOST /two-factor/disable {password:"wrong-password"}→ 400 INVALID_PASSWORD; rows 1, verified 1, two_factor_enabled 1; fresh sign-in still twoFactorRedirect true; caller's session still 200POST /two-factor/disable {password}→ 200{"status":true}+ rotatedbetter-auth.session_token;select count(*) from sys_two_factor where user_id=…= 0; sys_user.two_factor_enabled = 0POST /auth/sign-in/email→ 200 keys [redirect, token, user, session], no twoFactorRedirect, Set-Cookie onlybetter-auth.session_token;GET /api/v1/data/showcase_task→ 200PUT /api/settings/auth {values:{mfa_required:"everyone"}}→ 200 (levels off/administrators/everyone), grace 7, features.twoFactor true. Enrolled user: get-session authGate null, data 200, mfa_required_at NULL (enrolled user never stamped). Disable under mfa_required → 200{"status":true}(NOT refused), rows 0, two_factor_enabled 0; pre-disable bearer → 401 (rotated); stamp written lazily 2026-11-13T10:18:31.012Z at the first unenrolled sight. Re-sign-in → no twoFactorRedirect, get-session authGate null, data 200 → fresh 7-day window branch.mfa_grace_period_days:0→ 200; new session: get-session 200 user.authGate {code:MFA_REQUIRED, "Multi-factor authentication is required…"}, data read 403{"error":{"code":"MFA_REQUIRED",…}}(the earlier in-grace session also 403 now). While gated:POST /two-factor/enable→ 200 [method, totpURI, backupCodes], get-totp-uri 200, verify-totp 200 → authGate null, data 200 (not stranded). Stamp after re-enroll still 10:18:31.012Z (never cleared, as the source says). GatedPUT /api/settings/auth {mfa_required:"off"}→ 403 MFA_REQUIRED; after re-enroll the restore → 200, read back off / 7{password}→ 401; a challenge-cookie-only client (two_factor cookie, no session): disable{password}→ 401, generate-backup-codes 401, get-totp-uri 401, disable{}→ 400 VALIDATION_ERROR (body schema before auth, as on enable); the factor survived (still challenged). No findings.Fails
None.
Parked
previous_password_hashesring only holds passwords written whilepassword_history_count > 0: with the count armed after two resets, the then-current password was refused (PASSWORD_REUSE) but a password set while the count was 0 was accepted. The step's "resetting to the OLD password is refused" holds only for an old password set after arming; the step could say so. A PASSWORD_REUSE refusal leaves the token usable (the same token then reset successfully)./auth/configreportsfeatures.emailDelivery:falseon the stock boot while the reset mail is written to sys_identity_email with statussent(dev log transport). A runner reading emailDelivery:false as "no mail" would wrongly block the token capture; the fixture could mention it.PUT /api/settings/auth→ 403 MFA_REQUIRED) until they enroll again, so "restore both values afterwards" needs a re-enrollment first. The lazy stamp was written within the disable's own exchange (already present immediately after the 200), and survives re-enrollment.objectstack-fleet commentedon Nov 13, 2026
qa#3 · os-tesla · session_01L9qibw6yXcLi9aRRBpPT4W
qa#3 · 18.4.0 R2 part C · C-api · group C2 — subject c419609
Runner C2, 6 items, every revision as dispatched (area JSON matches: no-active-org-session-semantics 10, api-key-expiry-lifecycle 3, import-invitation-and-sso-domain-answers 3, idp-group-permission-set-mapping 1, ai-service-account-lifecycle 1, ai-service-account-register-and-key 1). Live boots, one at a time, all from the tree
examples/app-showcasewithOS_PORT=P OS_STORAGE_LOCAL_ROOT=/tmp/qa3b/C2/storage/<name> setsid pnpm exec objectstack dev --seed-admin -p P -d file:/tmp/qa3b/C2/db/<name>.db --no-compile --no-watch, stopped by process group (helpers /tmp/qa3b/C2/bin/boot.sh, stop.sh): :4820 noorg-isolated.db (walled isolated), :4821 noorg-group.db (walled group) — both + OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai OS_AUTH_MEMBERSHIP_POLICY=invite-only; :4822 identity.db (OS_SCIM_ENABLED=true; shared by api-key-expiry, both service-account items, idp-group mapping and the #33075 SCIM probe, each on its own personas/keys/names — deviation from one-DB-per-item, the service-account fixtures chain); :4823 sso-on.db (OS_SSO_ENABLED=true OS_SSO_DOMAIN_VERIFICATION=true) then sso-off.db (OS_SSO_ENABLED=true only), env read from the listener's /proc environ. No scratch copies needed. Server logs /tmp/qa3b/C2/logs/*.log. Pin logs /tmp/qa3b/C2/pins/.log (unit, one vitest run per package, --maxWorkers=1) and /tmp/qa3b/C2/pins/df/.log (dogfood, one file per invocation). Pin totals, all at c419609, 0 failed: plugin-security 2 files 29/0; rest 1 file 5/0; runtime 1 file 14/0; plugin-auth 10 files 197/0 (the 8 named by the items + the headline's scim-deactivation-reconcile-user.test.ts and api-key-restore-guard.test.ts); plugin-email 1 file 32/0; core 1 file (api-key-lifecycle.test.ts, headline corroboration) 25/0; dogfood 5 files: no-active-organization-write-refusal 6/0, sharing-rule-org-less-caller 16/0, api-key-lifecycle 3/0, service-account-register-key 10/0, service-account-lifecycle 5/0. (The non-TTY reporter prints per-package totals only; per-file counts not separated for multi-file packages.) No release blocker. No clause FAIL. One probe finding beside the clauses on the #33075 surface is handed to PM privately (security carve-out) — no detail here.access-security.no-active-org-session-semantics (rev 10, P0) — PARTIAL (every driven clause pass; one sub-leg, A0c, blocked(fixture) — knownGaps[0] producer (1))
pins: plugin-security no-active-organization-write-refusal.test.ts + no-active-organization-insert-union-scope.test.ts → 2 files 29/0 (plugin unit, stub terminal; and ENGINE, real ObjectQL + SqlDriver) · dogfood no-active-organization-write-refusal.dogfood.test.ts → 6/0 (HTTP, in-process posture-only isolated showcase) · sharing-rule-org-less-caller.dogfood.test.ts → 16/0 (HTTP, in-process showcase). No clause scored from the plugin-unit pin alone (rule 6): every clause below is scored on the live walled boots.
live: two walled boots from the tree, by hand (posture env +
--no-compile, notqa-fixture.mjs walled-boot, which bootsos devWITHOUT --no-compile — see Parked): OS_TENANCY_POSTURE=isolated on :4820 db /tmp/qa3b/C2/db/noorg-isolated.db, OS_TENANCY_POSTURE=group on :4821 db /tmp/qa3b/C2/db/noorg-group.db; both OS_PLATFORM_OWNER_EMAIL=admin@objectos.ai OS_AUTH_MEMBERSHIP_POLICY=invite-only. Wall proven each boot: bannerTenancy: isolated|group, 0 DEGRADED lines, GET /api/v1/auth/config → features {multiOrgEnabled true, tenancyPosture isolated|group, degradedTenancy false}. Operator = seeded admin, active org = Default Organization (A); operator created org B (POST /auth/organization/create 200) and staged account/project/task in B and account/project in A. Persona = producer (2): member minted by operator POST /auth/admin/create-user while A active → 200 {membershipCreated:true, organizationId A}; change-password; POST /auth/organization/set-active {"organizationId":null} → 200null; GET /auth/get-session → session.activeOrganizationId null (isolated M1 CPlmT4gY…, group G1 n5ps14Wd…). Producer (1) (member-less user) has no measured walled door (knownGaps[0]) → not driven. NULL-org counts read read-only from each boot's SQLite file over all 69 tables carrying organization_id (/tmp/qa3b/C2/bin/nullcount.py).200 {"records":[],"total":0,"hasMore":false}; by-id of a B task and of an A task → 404 RECORD_NOT_FOUND{"error":"[Security] Access denied: 'showcase_task' is scoped to an organization, and this session has no active organization — so this insert has no organization to place the record in. Join or select an active organization and retry.","code":"PERMISSION_DENIED","object":"showcase_task"}; same sentence for POST showcase_private_note, POST showcase_task/createMany [2 rows], and a create carrying a forged organization_id A or "". Control: same member with A active → showcase_private_note 201 org A, showcase_task 201 org A. (Driving note: PATCH/DELETE of an existing task and showcase_account create answer the generic 403 "You do not have permission…" with AND without an active org — the member CRUD gate, not this refusal; not scored){"code":"PERMISSION_DENIED","error":"sharing-rule administration requires the manage_sharing capability (ADR-0111 D6)"}; rule B still stored. Data: PATCH B's showcase_account and POST with organization_id B → 403; GET B account by id → 404; row unchanged. Control M1 with A active: systemPermissions [manage_sharing]; GET /sharing/rules → 200, rules all org A (qa_c2_rule_a + 2 shipped), B absent; by-name B → 404 RULE_NOT_FOUNDUnhandled/SystemWriteOrganizationRequiredlines in either boot logUSER_IS_NOT_A_MEMBER_OF_THE_ORGANIZATION(better-auth's own code, outside ADR-0112 — known shape, note only), session stays null; POST /api/v1/keys → 400 VALIDATION_ERROR "Cannot create an API key without an active organization: this deployment runs a walled tenancy posture ('isolated')…" (names the org); GET /data/showcase_task/ (empty id) → 404 ENDPOINT_NOT_FOUND; create with organization_id "" → the same 403; group member filtering on organization_id = B → 200 total 0. No finding.identity-auth.api-key-expiry-lifecycle (rev 3, P1) — PASS
pins: rest src/rest-api-key-lifecycle.pin.test.ts → 5/0 (unit: HTTP stub + fake ql) · runtime src/api-key-lifecycle.pin.test.ts → 14/0 (unit: fake objectql + fake kernel) · dogfood api-key-lifecycle.dogfood.test.ts → 3/0 (HTTP, in-process booted showcase) · live: :4822, db /tmp/qa3b/C2/db/identity.db (auth settings at defaults). Clock-skew trap ruled out: server
Dateheader 10:19:41 GMT = localdate -u10:19:41.{"code":"API_KEY_EXPIRY_TOO_LONG","message":"expires_at 2027-12-18T10:19:46.000Z is further away than this deployment allows: … Maximum key lifetime is 365 days, so the latest expiry a key signed now may have is 2027-11-13T10:19:46.393Z…"}; GET /api/v1/data/sys_api_key total 2 before, 2 after (control +365 d −1 min → 201){"error":{"code":"API_KEY_REVOKED","message":"This API key (osk_VxF65Bnv…) was revoked. Its owner or a platform administrator can restore it…"}}; key minted with expires_at +5 s used 200, after lapse → 403{"error":{"code":"API_KEY_EXPIRED","message":"This API key (osk_RZnpzv_H…) expired on 2026-11-13T10:19:56.000Z. Keys do not renew…"}}; made-up osk_ key → 401 UNAUTHENTICATED (distinct)identity-auth.import-invitation-and-sso-domain-answers (rev 3, P2) — PASS
pins: plugin-auth sso-domain-verification-unknown-provider.pin.test.ts, admin-import-users-invite-link.test.ts, admin-import-users-account-invitation.test.ts (in the 10-file plugin-auth run 197/0; unit — the SSO pin is real AuthPlugin on Hono over createMemoryEngine) · plugin-email auth-account-invitation-template.test.ts → 32/0 (unit) · live: :4823 sso-on.db (OS_SSO_ENABLED=true OS_SSO_DOMAIN_VERIFICATION=true) and sso-off.db (OS_SSO_ENABLED=true only); env proven from /proc//environ. OS_SSO_ENABLED set on both (the door is SSO-gated; see Parked) so only the domain-verification flag differs.
{"code":"RESOURCE_NOT_FOUND","message":"SSO provider \"qa-no-such-provider\" was not found."}; verify-domain same. OFF: both → 400{"code":"DOMAIN_VERIFICATION_DISABLED","message":"Domain verification is not enabled (set OS_SSO_DOMAIN_VERIFICATION)."}identity-auth.idp-group-permission-set-mapping (rev 1, P1) — FAIL (SCIM legs pass; SSO leg blocked(fixture); one security row)
pins: plugin-auth scim-group-projection.test.ts (in the 10-file plugin-auth run 197/0; plugin unit + engine over a SQL driver, AuthManager.handleRequest()) — no pin signs in through a live IdP · live: :4822 identity.db, OS_SCIM_ENABLED=true. SCIM credential: POST /auth/admin/scim/mint-credential {connectionId:"qa-c2-okta"} → 200 {credentialId zMkaLJFp…, tokenHint, expiresAt null} (bearer shown once, not recorded). Provisioned user g1: POST /auth/scim/v2/Users (bearer) → 201, SCIM id LirVz14E…, sys_user hMTj9nOD… source idp_provisioned. Org admin persona: qa.c2.second, POST /auth/organization/update-member-role {role admin} → 200; /auth/me/permissions positions [org_admin, everyone] (no platform admin). Scratch set qa_group_reader (PUT /meta/permission, objects {showcase_task {allowRead}}) → 200.
{"code":"PERMISSION_DENIED","message":"Admin role required"}(anon → 401); platform admin same body with admin_full_access → 400 VALIDATION_FAILED fields[{field permissionSet, code rule_violation, "…never administrator standing; refused: permission set 'admin_full_access', which is outside the organization boundary by name…"}]; platform admin qa_group_reader → 200 {mappingId i8Hp1GC2…, created true, organizationId = the connection's org, reprojected true}identity-auth.ai-service-account-register-and-key (rev 1, P1) — PASS
pins: dogfood service-account-register-key.dogfood.test.ts → 10/0 (HTTP, in-process booted showcase) · plugin-auth service-account-doors.test.ts + (A4) service-account-sign-in-refusal.test.ts + last-admin-guard.agent-account.test.ts (in the 10-file run 197/0; unit) · live: :4822 identity.db. Personas via admin POST /auth/admin/create-user + change-password: owner s7PAJ4Vh…, second irEJhZ4H…, third it8vh3vW…, outsider 5230GOw8… (then POST /auth/organization/remove-member → sys_member rows 0).
keyfieldkeyis a 64-char digest ≠ raw keyidentity-auth.ai-service-account-lifecycle (rev 1, P1) — PASS
pins: dogfood service-account-lifecycle.dogfood.test.ts → 5/0 (HTTP) · plugin-auth service-account-lifecycle.test.ts (in the 10-file run 197/0; unit) · live: :4822 identity.db; account njDs…, K1 (owner-minted), K2 (admin-minted); reads = GET /data/showcase_task?limit=1 with each key, after a scratch grant qa_c2_task_reader so an admitted key answers 200 (without it the agent ceiling answers 403 for an admitted key, which would not tell 200 from 403 — see Parked).
#33075 headline probe (rule 8, ~6 min, on :4822 identity.db — scored nothing; doors: SCIM PATCH/DELETE /Users, REST data, MCP, the restore doors, the audit ledger)
SCIM user d1 (bnM358o6…, provisioned through qa-c2-okta) given a local password by POST /auth/admin/set-user-password → 200 (non-cloud-managed), signed in, minted 3 keys over POST /keys: live, one self-revoked (reason null), one admin-revoked (administrator_suspended); controls: third's key, the admin's key. SCIM PATCH active:false → 200: d1 banned ("Deactivated via SCIM"), session token → 401; all 3 d1 keys revoked:1 revoked_reason scim_deactivated (both earlier reasons restamped); each key → REST GET /data/showcase_task 403 API_KEY_REVOKED "…revoked because the organization's directory deactivated its owner, and it cannot be restored, even after the owner is re-activated. Its owner creates a new key…", and MCP POST /api/v1/mcp tools/call query_records → 403 same code; third's and admin's keys 200 on both doors (untouched). Admin restore via data door PATCH {revoked:false} → 403 PERMISSION_DENIED (names the reason and the new-key road); service-account/restore-key on a person's key → 404 (not its door). Audit: one sys_audit_log
updaterow per revoked key, object sys_api_key, actorsvc:scim:qa-c2-okta, metadata {scim_connection_id}, tenant_id = org, user_id null. SCIM PATCH active:true → 200: ban lifted, every old key still revoked (REST + MCP 403); owner restore ×2 and admin restore → 403; an admin re-revoke keeps scim_deactivated; a key minted after re-activation → 200 on REST and MCP. SCIM DELETE /Users/{id} → 204: user banned again, the post-reactivation key restamped scim_deactivated → 403. Noerrorincomplete-revocation line in the log. The commit's rule holds on every door tried. Note: the dispatch expected 401 on a revoked key; the door answers 403 API_KEY_REVOKED by design (core api-key.ts) — distinct from the 401 an unknown key gets. The security row from this probe is S1 on identity-auth.idp-group-permission-set-mapping.Fails
None. No clause failed on any item. The security row sits on identity-auth.idp-group-permission-set-mapping (S1).
Parked
scripts/qa/qa-fixture.mjs walled-bootbootsos devwithout--no-compile(its argv:dev --ui --seed-admin -p … -d …), so on a shared checkout it rewritesexamples/app-showcase/dist/objectstack.jsonunder every other runner — the RUNNER.md "boot with --no-compile" entry's hazard, inside the very command RUNNER.md and this item's knownGaps[2] point runners at. C2 booted the two walled postures by hand instead (posture env +--no-compile). Suggest the fixture pass--no-compile(or take a flag), and RUNNER.md's walled-boot entry say so.project(400 VALIDATION_FAILED without it). The operator staged account/project; a runner reading the generic 403 as "the refusal" would mis-score A1. Suggest steps name showcase_task (with a project) / showcase_private_note as the tenant-scoped write targets. Same for update/delete: PATCH/DELETE of an A task by a plain member is the CRUD 403 whatever the org state.OS_SSO_DOMAIN_VERIFICATION; the domain doors are SSO-gated and needOS_SSO_ENABLED=true(or plugins.sso) on BOTH boots — the requires line should name it (as the dispatch also noted). sys_identity_email has no template-id column, so A1's "template id" verify is read off subject/body.409 UNIQUE_VIOLATIONon (user, set, org). The pin's CONTROL makes the hand grant FIRST and shows the projection writes nothing beside it. Suggest the step say: hand-grant first, then save/push, then delete. Wording note: an unknown permission-set name is refused with the boundary sentence ("…never administrator standing; refused: permission set 'no_such_set', which is outside the organization boundary…") rather than "no such permission set" — accurate-by-construction but misleading; not filed.403 API_KEY_REVOKEDon REST and MCP, not 401 — by design; the clause wording any future item gets for this rule should expect 403 with thescim_deactivatedtext. Checklist gap: no item asserts the SCIM-deactivation key revocation live (the headline change has unit pins only: scim-deactivation-reconcile-user, api-key-restore-guard, core api-key-lifecycle); the probe above is a ready recipe (mint credential → POST /Users → admin set-user-password → keys → PATCH active false/true → DELETE).objectstack-fleet commentedon Nov 13, 2026
qa#3 · os-tesla · session_01L9qibw6yXcLi9aRRBpPT4W
qa#3 · 18.4.0 R2 part C · C-api · group C3 — subject c419609
Runner C3 used ports 4830–4834 (plus 4839 for a local delay server) out of 4830–4839. Only one showcase server was up at a time. Every boot was
setsid, and each process group was stopped when its item finished. Every tree boot wasOS_PORT=$P … objectstack dev --seed-admin -p $P -d file:/tmp/qa3b/C3/db/<DB>.db --no-compile --no-watch --log-level info, run fromexamples/app-showcase.Boots and DBs
durable.db: the P0 item. It was booted twice over the same file, with a cold restart in between.stockgroup.db: one shared-fixture group for packaged-flow-write-door-parity, then flow-toggle-kill-switch, then flow-run-semantics-v18 (live legs).OS_ALLOW_UNLINTED_METADATA_WRITES=1for the e003ebb rule-8 probe only. That rule-8 probe needed a read-only write that the author-time lintflow-update-readonly-fieldotherwise refuses at save. No clause was scored on the relaxed boot.trigger.db: trigger-status-contract. The telemetry DB wastrigger.telemetry.db, and the delay server ran on 127.0.0.1:4839./tmp/qa3b/C3/ws/examples/app-sub, DBsubflow.db: packaged-flow-subflow-disable-refusal. It needed install-local, which writes into the app dir. The copy hasws/packagesandws/node_modulessymlinked to the tree.sla.db: approvals.sla-escalation, run under the wall-clock offset shimNODE_OPTIONS=--require /tmp/qa3b/C3/sla/clock-shim.cjs(a copy of T3's), with offset file/tmp/qa3b/C3/sla/offset. The same boot also hosted the 9df85e1 MCP rule-8 probe./tmp/qa3b/C3/ws/examples/app-cliwas used for the SLA build oracle.Evidence locations
/tmp/qa3b/C3/<group>/*.log/tmp/qa3b/C3/bin/*.mjs. Some are adapted from T2-B's and T3's recipes, with ports and paths changed.Pins
--maxWorkers=1. Dogfood ran one file per invocation. Logs are in/tmp/qa3b/C3/pins/*.logand the summary is in/tmp/qa3b/C3/pins/summary.txt.Other checks
examples/app-showcase/.objectstack/holds onlydata/, so nothing was installed into it.Wall-clock: about 6 min setup (10:10–10:12 reading and helpers, plus the scratch copies) and about 22 min testing (10:12–10:34 UTC, including roughly 5 min waiting on the SLA periodic tick).
automation.packaged-flow-disable-durable (rev 10, P0) — PASS
pins: none (manual) · live: :4830,
durable.db, two boots over the same file, both--log-level info· adminBaseline: urgent create 7IzIxOWY… → runs 0 → 1 (run_fa9c5822… completed).
{name, enabled:false}. GET /data/sys_metadata_activation → total 1: row Sp9nVaxSywmrcvy-, metadata_type=flow, name=showcase_urgent_task_alert, package_id=com.example.showcase, active=false. KEY SET = [active, created_at, created_by, id, metadata_type, name, package_id, updated_at, updated_by], and'organization_id' in row= false{code:FLOW_DISABLED, message:"This flow is turned off for this deployment. An administrator can turn it on again in its package's Automations in Studio, or copy it there under a new name."}. Exact string match true; nosys_metadata_activation, noADR{enabled:false, bound:false, status:active, triggerType:record_change}. server-1.log:1042INFO [Automation] Activation ledger: 1 packaged flow(s) are switched off for this installation and were left unbound — 'showcase_urgent_task_alert'.Probe (~2 min; admin, phone.demo member, anonymous; toggle and ledger data doors):
{enabled:"false"}→ 400 VALIDATION_FAILED.%20→ 404 "Flow ' ' not found".manage_metadatacapability", and /_status was unchanged.No findings.
automation.flow-run-semantics-v18 (rev 3, P1) — PASS
pins:
live: :4831,
stockgroup.db. Scratch flows were registered at runtime by the admin through POST /automation: qa_c3_stamp (record-after-update on showcase_private_note, conditiontitle != previous.title, update_record body = CELcurrent_user.id), qa_elevated (autolaunched, runAs system, create_record), and qa_c3_decision (decision with edges1 == 1then2 == 2). The member qa-c3-mem-… was made with admin/create-user (mustChangePassword:true), then change-password, then a fresh sign-in.skipped. GET run steps = [start:success, d:success, a:success, b:skipped]; FIRST rows 1, SECOND rows 0{record:{id}}and body{runAs:'user'}→ still 403; no run, no rowProbe (~1 min):
No findings.
automation.trigger-status-contract (rev 14, P1) — PASS
pins: none · live: :4832,
trigger.db(telemetrytrigger.telemetry.db), delay server 127.0.0.1:4839 · admin + contributor member qa-contributor-20261113101932@verify.test (node scripts/qa/qa-fixture.mjs contributor-member; not the trigger identity)Recipe qa-flow-status-doors, plus the scratch flows qa_screen_child, qa_map_parent (map → qa_screen_child) and qa_resume_inprog (screen → http GET :4839/slow?s=6 → screen):
flow-start-node-missing; GET /meta/flow/qa_no_start_node → 404| # | clause (short) | verdict | evidence |
|---|---|---|---|
| A0 | every trigger row, both routes | pass | live, identical at POST /automation/:name/trigger and /automation/trigger/:name. Unheld → 404 RESOURCE_NOT_FOUND "Flow 'qa_unheld_flow_xyz' not found". qa_no_start_node → 404 RESOURCE_NOT_FOUND "Flow 'qa_no_start_node' not found" (not 422, not 200). Urgent alert (off) → 409 FLOW_DISABLED. qa_input_schema_invalid → 422 FLOW_INPUT_SCHEMA_INVALID "Node 'assign' parameter 'assignments' expected type 'string' but got 'object'". qa_always_fails → 400 FLOW_FAILED "Node 'boom' failed: http: fetch failed — api.example.com: getaddrinfo ENOTFOUND". showcase_notify_owner {params:{ownerId, message}} → 200, data.success true. No refusal row answered 200 |
| A1 | PAUSED = 200 + runId + screen | pass | live: showcase_reassign_wizard {params:{recordId, objectName}} → 200
{status:'paused', runId:run_65f6d270…, screen.nodeId:'collect'}(legacy route → run_08d04029…). run_65f6d270 later resumed 200 → completed, task.assignee 'qa-c3-assignee' || A2 | cross-door parity for one engine exit | pass | live: wizard OFF → POST /actions/showcase_task/showcase_bulk_reassign → 409 FLOW_DISABLED, and the trigger door → 409 FLOW_DISABLED. showcase_inquiry_purge OFF → POST /apps/showcase/inquiries/purge → 409 FLOW_DISABLED, and the trigger door the same; closed inquiries 1 before, 1 after. Authored qa_fail_action and qa_schema_action (PUT /meta/action/…?package=…, 200): /actions → 400 FLOW_FAILED and 422 FLOW_INPUT_SCHEMA_INVALID. Both flows toggled back ON |
| A3 | 400 carries details.errorMessage + summary | pass | live: trigger door (both routes) details.errorMessage "QA always-fails probe: the unroutable http node failed" and details.summary.nodes [start success, boom http failure]. The /actions door has the same two keys; its message is prefixed "Flow 'qa_always_fails' failed: …" |
| A4 | resume refusals; retryable arms keep the suspension | pass | live on run_65f6d270: see "A4 resume arms" below |
| A5 | stale suspension → 404 RUN_NOT_FOUND naming the cause | pass | live: paused qa_screen_child run_1ef69768; DELETE /automation/qa_screen_child → 200; resume → 404 RUN_NOT_FOUND "Flow 'qa_screen_child' not found for run 'run_1ef69768…'". This is the deregistration arm; the edited-node arm was not driven (Parked) |
| A6 | SDK rejects with httpStatus + code | pass | live: @objectstack/client (packages/client/dist/index.mjs) against :4832. trigger() and execute() REJECT 404 RESOURCE_NOT_FOUND ×2 (unheld, qa_no_start_node), 409 FLOW_DISABLED, 422 FLOW_INPUT_SCHEMA_INVALID and 400 FLOW_FAILED, and RESOLVE the 200 and the paused run. resume() REJECTs 403 PERMISSION_DENIED, 400 INVALID_SCREEN_INPUT, 400 INVALID_SIGNAL, 404 RUN_NOT_FOUND and 503 STORE_UNAVAILABLE, then RESOLVES the valid resume |
| A7 | start-less flow refused where authored | pass | live: POST /automation qa_no_start_node_save → 400 VALIDATION_FAILED "Flow 'qa_no_start_node_save' rejected: the flow's top-level
nodeshold nostartnode (found 1 node(s), of typeend)…"; GET /automation/qa_no_start_node_save → 404. Recipe PUT → 422 INVALID_METADATA, ruleflow-start-node-missing; GET /meta/flow/qa_no_start_node → 404 || N0 | (neg) 200 with an inner success:false | pass | none across every row and door |
| N1 | (neg) never-dispatched exit → 400 FLOW_FAILED | pass | disabled → 409, schema → 422, unheld and no-start → 404, at every door |
| N2 | (neg) 409/422 collapsed | pass | distinct at the trigger and /actions doors |
| N3 | (neg) paused answered as a refusal | pass | A1 |
| N4 | (neg) a retryable refusal consumed the suspension | pass | A4: every re-read
paused, and every later valid resume 200 || N5 | (neg) a door with its own table | pass | source: domains/automation.ts:3158, action-execution.ts:2859 and endpoint-executor.ts:633 each call classifyFlowRefusal, imported from flow-dispatch-status.ts (FLOW_NOT_FOUND_STATUS at :140, classifyFlowRefusal at :194) |
A4 resume arms, live on run_65f6d270 unless noted:
inputs:{}→ 400 INVALID_SCREEN_INPUT.{inputs:{new_assignee}, output:{$x:1}}→ 400 INVALID_SIGNAL "(collect.$x)".output-only → 400 INVALID_SCREEN_INPUT.$inside inputs → 400 INVALID_SCREEN_INPUT.inputs+variables→ 400 VALIDATION_FAILED.pausedafter each refusal. The valid resume after the rename back → 200 completed.failed, and a re-resume → 404 RUN_NOT_FOUND.Teardown: urgent alert, wizard and inquiry purge back ON; DELETE /packages/com.objectstack.qa.automation → 200 (7 items); the telemetry table renamed back each time.
Probe (~2 min, admin + member):
/automation//trigger→ 404 ENDPOINT_NOT_FOUND. runId%20→ 404 RUN_NOT_FOUND.[]→ 400 VALIDATION_FAILED.params:"x"→ 400 VALIDATION_FAILED. Malformed JSON → 400 INVALID_REQUEST.No findings.
automation.packaged-flow-subflow-disable-refusal (rev 12, P1) — PASS
pins: none · live: :4833, scratch copy
/tmp/qa3b/C3/ws/examples/app-sub,subflow.db, adminN1 detail: install-local → 200 hotLoaded:true. GET /automation/qasub_second_notify_caller → status active, _provenance 'package', _packageId com.objectstack.qa.qasub. Child disable → 409 DELETE_RESTRICTED "…while 2 packaged flows still call it as a subflow: 'showcase_task_done_notify_owner', 'qasub_second_notify_caller'…". The child's ledger row was unchanged (active true, updated_at 10:21:09.491Z before and after). Teardown: DELETE /marketplace/install-local/com.objectstack.qa.qasub → 200, after which the same disable names showcase_task_done_notify_owner alone.
Probe (~1 min, admin, toggle and create doors):
%20→ 404.enabled:"yes"→ 400 VALIDATION_FAILED. Body {} → 200 enabled:true (the documented "empty body = enable" contract).installed-packages/held onlydataafter the teardown.No findings.
access-security.packaged-flow-write-door-parity (rev 4, P1) — PASS
pins: none (manual) · live: :4831,
stockgroup.db, seeded admin (holds manage_metadata)Setup: GET /automation/showcase_urgent_task_alert captured (sha256 2e06b891b160… →
/tmp/qa3b/C3/stock/capture.json). GET /meta/flow/…?layers=true → code._packageId com.example.showcase, _provenance package, lock "full", editable false, deletable false.manage_metadatacapability"Probe (~1 min, admin + member):
{}→ 403 NOT_OVERRIDABLE (the lock answers before any body check). PUT with a body renaming the flow → 403 NOT_OVERRIDABLE.No findings.
automation.flow-toggle-kill-switch (rev 4, P2) — PASS
pins: none · live: :4831,
stockgroup.db(after the parity item), adminProbe (~1 min, admin): OFF twice → 200 both; ON twice → 200 both. After that, one urgent create gave exactly one new run (3 → 4), so there is no double binding. A non-urgent create fired nothing (4 → 4). No findings.
approvals.sla-escalation (rev 4, P2) — PASS
pins: none · live: :4834,
sla.db, wall-clock offset shim (/tmp/qa3b/C3/sla/clock-shim.cjs, offset file/tmp/qa3b/C3/sla/offset; the server's environ was checked for NODE_OPTIONS and QA_CLOCK_OFFSET_FILE). Offsets: 0 → +2h (restart) → +3.5h (no restart; one periodic tick) → +5h (restart). Timers untouched.Personas: admin (submitter, flow author), phone.demo (approver), auditor.demo (escalatee).
Fixture: four runtime flows registered through POST /api/v1/automation, one per action, each on its own record-after-create object. Approver usr_showcase_phone_demo; escalation {enabled:true, timeoutHours:1, action, notifySubmitter:true}.
Positive controls, because the sweep logs nothing: control1 was opened at +2h (due +3h) and must be escalated by the PERIODIC tick at +3.5h. control2 was opened at +3.5h (due +4.5h) and must be escalated by the BOOT sweep at +5h.
actor_idkey is ABSENT on the /actions read. Re-read after the periodic tick at +3.5h (10:30:09 real), and again after the +5h restart: still 1 row each. Both sweeps are proven to have run by their positive controls: control1 areq_af86060c got its escalate row at shim 14:00:09 (the periodic tick), and control2 areq_41cec0d3 at shim 15:31:05 (the boot sweep)os validatewith escalation {…, bogusKey:1} → exit 1: "✗ src/automation/flows/qa-c3-bad-escalation.flow.ts:2:38 · defineFlow validation failed … nodes.1.config.escalation.bogusKey: … Unrecognized key(s) on this approval escalation:bogusKey… Until this shape was closed, these were dropped silently". Alias {timeout:1} → exit 1 with 2 issues: "escalation.timeoutHours … requires" and "escalation.timeout … Did you meantimeout→timeoutHours?". Positive control: the same file with a valid escalation → "✓ Validation passed", exit 0. Runtime door too: POST /automation with bogusKey → 400 VALIDATION_FAILED at nodes.1.config.escalation.bogusKey, and GET → 404Probe (~1 min, phone.demo and auditor.demo):
No findings.
Increment probes (rule 8) — e003ebb and 9df85e1
These are probes, not clauses, so they score nothing.
e003ebb (#33068) — map items and resumed subflow children name their dropped read-only fields. ~4 min, admin, :4831.
showcase_contact.lead_scoreis refused at save: 422 INVALID_METADATA, ruleflow-update-readonly-field. So the runtime drop is reachable only past the lint. The probe was therefore driven on a reboot of the same DB withOS_ALLOW_UNLINTED_METADATA_WRITES=1, the save-door relaxation L3 also used.droppedFields:[{object:'showcase_contact', fields:['lead_score'], reason:'readonly'}], and the stored lead_score stays null.droppedFieldsmember.[{showcase_contact, [lead_score], readonly}]. A map whose child drops nothing → member absent; an empty list → absent.steps[sub].droppedFields = [{showcase_contact, [lead_score], readonly}]. So creditChildRun credited the awaiting step on the up-bubble path. The parent's own answer is not returned on this path, so only the step is observable over HTTP.No findings. The failed-child and cancelled-map-item gaps the commit documents were not probed.
9df85e1 (#33076) — submitter's agent recall/resubmit over MCP. ~4 min, :4834 at offset 0, live MCP transport (POST /api/v1/mcp tools/call run_action).
recall:usr_showcase_auditor_demo.submit,revise:phone,resubmit:auditor.recordId; a stranger's REST POST /approvals/requests/:id/recall → 404 REQUEST_NOT_FOUND, the same as over MCP.No findings. One wording note in Parked.
Fails
None. No clause failed, and no product defect was found beside the clauses.
Parked
FORBIDDEN) and the request does not move". Live, a caller who cannot read the request (not submitter, approver or admin) getsREQUEST_NOT_FOUND(404) from the read gate first, over MCP and REST alike. Only a participant who is not the submitter (the approver) sees FORBIDDEN. The behaviour is the safer one (no existence signal). The doc sentence is imprecise rather than wrong for the participant case. A docs-accuracy note for the anchor; nothing to extract.flow-update-readonly-fieldrefuses the write at author time. It reaches a live run only past the lint (OS_ALLOW_UNLINTED_METADATA_WRITES=1, a flow stored before the rule, or a host executor). The commit's pins (run-dropped-fields.test.ts, 18 tests in the 72/0 run) are engine-level. A future item that scoresdroppedFieldsover HTTP needs that fixture named.qa_elevatedwith no recipe body. This run authored it the same way L3 did (autolaunched, runAs system, create_record).objectstack-fleet commentedon Nov 13, 2026
qa#3 · os-tesla · session_01L9qibw6yXcLi9aRRBpPT4W
qa#3 · 18.4.0 R2 part C · C-api · group C4 — subject c419609
Runner C4, sequential (one server of mine up at a time after a helper fix noted under the stdio item). Ports 4840–4847 (stdio provisioning
os dev4840 +os startstdio legs 4841 · OAuth boots 4842 · run-action + verb-gate shared boot 4843 · route ledger 4844 · named import 4845 · declarative-boot scratch 4846 · degraded-recovery copies 4847). One file DB per item under /tmp/qa3b/C4/db/ (stdio, oauth, apigrp [shared-fixture group: ai.mcp-run-action-exposure-gate + api-backend.api-methods-verb-gate, disjoint fixtures], ledger, import, conn, conn-d, conn-broken, conn-cfgfault). Tree boots: the brief'sos dev --seed-admin --no-compile --no-watchline from examples/app-showcase (helper /tmp/qa3b/C4/bin/boot.sh); stdio legs onos startfrom the same dir with--home /tmp/qa3b/C4/stdio/home. Scratch copies under /tmp/qa3b/C4/ws/examples/ (ws/packages + ws/node_modules symlinked): app-plant, app-plant4, app-write, app-write3 (migrate CLI only), app-conn (connector cases, compiled under OS_PORT=4846); degraded-recovery used qa-fixture copies /tmp/qa3b/C4/{brk,cfg} (torn down). Pins (Tier 1, one vitest run per package, --maxWorkers=1, dogfood alone): packages/mcp 2 files 27/0 (plugin.test 18 + plugin-execution-context 9) · packages/plugins/plugin-auth 2 files 114/0 (auth-manager.mcp-oauth 88 + mcp-oauth-plaintext-notice 26) · packages/spec 2 files 47/0 (api-derivation 32 + connector-provider 15) · packages/services/service-automation 1 file 110/0 · packages/qa/dogfood showcase-declarative-mcp 2/0 — total 9 files, 300 passed / 0 failed; logs /tmp/qa3b/C4/pins/*.log (summary.txt). All nine revisions match the area JSON at c419609 (9, 6, 7, 4, 4, 7, 6, 7, 1). Hygiene at the end: ports 4840–4849 free, no process of mine left,git status --porcelainempty, examples/app-showcase/.objectstack/ holds only data/ (no installed-packages). Wall-clock: ~6 min setup (brief, items, recipes, helpers; 10:09–10:13 plus helper writing interleaved) · ~34 min testing (pins 10:11:30–10:13:05, live 10:13–10:47 UTC).ai.mcp-stdio-fail-closed (rev 9, P0) — PASS
pins: packages/mcp src/tests/plugin.test.ts → 18/0, src/tests/plugin-execution-context.test.ts → 9/0 (PLUGIN UNIT, mocked ctx — corroborating only, scores nothing; log /tmp/qa3b/C4/pins/mcp.log, per-file counts mcp-verbose.log) · live: provisioning on
os dev:4840, every stdio leg onos start -p 4841spawned by a scripted stdio client (/tmp/qa3b/C4/bin/stdio.mjs, stdout = JSON-RPC only;--home /tmp/qa3b/C4/stdio/home, fixed--auth-secret), db file:/tmp/qa3b/C4/db/stdio.db. Personas: contributor-bound member qa-c4-contrib@verify.test (userId jtZh5nF0…,qa-fixture.mjs contributor-member, its subset proof passed: 0 seeded invoices, INV-1003 not served), client_liaison member qa-c4-liaison@verify.test (same fixture, its sys_user_position row PATCHed to showcase_client_liaison; REST read of showcase_project then 20 fields, no budget/spent/budget_remaining), admin. Three osk_ keys via POST /api/v1/keys (201). Logs /tmp/qa3b/C4/stdio/logs/*.{out,err,json}. ⚠ Driving note: my boot helper's pid capture was wrong at first, so the :4840os devprovisioning server stayed up (same DB file) through the stdio legs; every MCP/REST leg below was served by the :4841os startprocess (revoke PATCH included), so no verdict rests on the :4840 process — recorded for honesty, helper fixed before the next item.os startOS_MCP_STDIO_ENABLED=true, key unset: exit 1 at 10 s, 0 stdout lines, /health 000; stderr "[MCP] The stdio transport is enabled (OS_MCP_STDIO_ENABLED / autoStart) but OS_MCP_STDIO_API_KEY is not set. stdio must run under a real identity — mint an API key on the Connect an AI Assistant page … or POST /api/v1/keys, and set OS_MCP_STDIO_API_KEY=osk_.... Refusing to start an unscoped stdio server (ADR-0101)." then "✗ Plugin com.objectstack.mcp failed to start - rollback complete" (pin plugin.test.ts corroborates the var name at plugin unit)--log-level info: "INFO [MCP] stdio transport principal-bound to OS_MCP_STDIO_API_KEY identity jtZh5nF0gUZgCEW4tU3ccwDsNjJCP2XA (RLS/FLS/tenant applied)" = contributor userId; liaison boot kW7DQE7o… = liaison userId; admin boot gkYHZap3… = adminRecord "HYD42rTyfi6RyjRK" not found in "showcase_invoice", REST 404 RECORD_NOT_FOUND. Admin key: MCP 14 rows incl INV-1003, get_record INV-1003 served; REST 14 rows + 200. FLS (liaison key): showcase_project MCP 5 rows / 20 fields without budget, spent, budget_remaining; REST identical 5 names and identical 20-field list; admin MCP and REST 23 fields incl the threeos startwith neither var: --log-level info boot → /health 200 at 10 s, 1× "INFO [MCP] Transport not auto-started (HTTP is served per-request at /api/v1/mcp regardless). Set OS_MCP_STDIO_ENABLED=true or autoStart …", 0 mentions of OS_MCP_STDIO_API_KEY, 0 stdout lines; default warn boot → /health 200, 0 such linesai.mcp-oauth-private-host-transport (rev 6) — PASS
pins: packages/plugins/plugin-auth src/auth-manager.mcp-oauth.test.ts → 88/0, src/mcp-oauth-plaintext-notice.test.ts → 26/0 (PLUGIN UNIT — corroborate only, score nothing, rule 6; logs /tmp/qa3b/C4/pins/plugin-auth.log, auth-verbose.log) · live: port 4842,
os dev --no-compileboots differing only in OS_AUTH_URL, each log captured from line 1 (/tmp/qa3b/C4/oauth/{a,b,b2,c,cw,https,p172,pfd,pfec}.{log,trace}), db /tmp/qa3b/C4/db/oauth.db. Address NOT held: the host holds 127.0.0.1 only; 10.0.0.5 / 192.0.2.2 are configured literals (the item permits it); requests went to localhost:4842 and were scored on the ADVERTISED origin. steps[3] second-machine client: blocked(fixture) — no second machine; it scores no clause.OAuth is served UNENCRYPTED: (a) 1 at WARN naming http://10.0.0.5:4842/api/v1/auth; (c) --log-level info: 1 at INFO naming http://localhost:4842/api/v1/auth (641 INFO lines = positive control); default warn-level loopback boot (cw): 0; (b) 0; https 0.OAuth discovery is served over PUBLIC plain HTTP: (b) 1, (a) 0, (c) 0, https 0. 「OAuth 未加密」 0 on every bootgit grep -l OS_ALLOW_INSECURE_OAUTH_HTTP→ only ai.json, plugin-auth CHANGELOG.md and the two test files — read by no sourceissuer: "https://10.0.0.5:4842/api/v1/auth"while the PRM's authorization_servers and every AS endpoint are http://; loopback (c) is http throughout. Same on public boot (b): the AS metadata issuer is https://192.0.2.2:4842/… while its PUBLIC warning names the http issuer.ai.mcp-run-action-exposure-gate (rev 7) — PASS
pins: none · live: port 4843, db file:/tmp/qa3b/C4/db/apigrp.db (shared-fixture group with api-backend.api-methods-verb-gate — one boot, disjoint fixtures) · MCP Streamable HTTP POST /api/v1/mcp (initialize → tools/call); admin osk_ key and Mei Phone osk_ key minted via POST /api/v1/keys (201); admin session token from sign-in; qa_ai_gated staged per fixtures.requires[3] (PUT /meta/action/qa_ai_gated → 200 seq 1), DELETEd in teardown; task LYbbLBIe25mXidzz (first
todotask: status todo, progress 0). Traces /tmp/qa3b/C4/apigrp/list-*.txt, server.logai: { exposed: true, description: … }"} (exact); REST re-read before/after: status todo, progress 0, done false, updated_at 10:34:29.098Z unchangedobjectName." (counts 47/35 vs 45/33 at 8e3d4a5 — more sys_* exposed entries on this tip; not a clause concern)[action-audit] MCP run_action 'showcase_portfolio_snapshot' on 'global' — body executes TRUSTED (system-elevated context, RLS/FLS-bypassing) for user 'yanpiqSTHcbseO0KWNi99TFzXF2XDfpf'= admin id from GET /auth/get-sessionAction "create_api_key" on system object "sys_api_key" is not exposed via MCP: a system object's action is reachable only when its author exposed it to AI (ai.exposed) and list_actions lists it for you(exact); without → 400 VALIDATION_ERROR "Action 'create_api_key' is on a system object ('sys_api_key') and is not exposed via MCP — a system object's action is reachable here only when its author declaresai: { exposed: true, description: … }on it" (exact); admin sys_api_key rows 2 → 2, qa-must-not-exist 0 → 0. EXPOSED: revoke_api_key listed on sys_api_key aiExposed:true requiresConfirmation:true; probe key ekQdomSTlvGtlWqf (POST /keys 201); admin SESSION bearer run_action {revoke_api_key, sys_api_key, recordId, confirm:true} → ok:true, result.record.revoked true; GET /data/sys_api_key/ekQd… → revoked trueapi-backend.api-methods-verb-gate (rev 4) — PASS
pins: packages/spec src/data/api-derivation.test.ts → 32/0 incl. 'a non-array apiMethods resolves to deny-all, not unrestricted' (SPEC UNIT — the layer the clause names for that one arm; run together with connector-provider.test.ts, 2 files 47/0, log /tmp/qa3b/C4/pins/spec.log + spec-verbose.log) · live: port 4843, db file:/tmp/qa3b/C4/db/apigrp.db (shared boot with ai.mcp-run-action-exposure-gate) · admin, Mei Phone, Ada Auditor (session tokens), anonymous · scratch objects via PUT /api/v1/meta/object/ {sharingModel:'public_read_write', fields.name, enable.apiMethods} (200), all four DELETEd in teardown (200). Traces /tmp/qa3b/C4/apigrp/vg-*.txt
Local closure (built @objectstack/spec/data effectiveOperationsArray∘resolveEffectiveApiMethods): ['get','list','create'] → ["get","list","create","aggregate","search","import","export"]; +bulk → ["get","list","create","bulk","aggregate","search","import","export"]; [] → []; undefined → 13 ops incl. restore, purge; ApiMethod.options = 6 (get,list,create,update,delete,bulk) = enumSource.expect.
upsert: declare ['create','update'] —upsertderives from create ∧ update / ⚠ After stripping, this whitelist is EMPTY —[]means DENY-ALL (fully closed API)…" + "[Registry] Object "qa_verb_legacy_upsert" declares retired legacy apiMethods value(s) [upsert] … ⚠ Ignoring them leaves NO primitives — this object's API resolves to deny-all (fully closed)…"; runtime 5 verbs 405 allowed []. ['get','history'] → 200; "•history: declare ['get'] withenable.trackHistory: true—historyderives from get ∧ trackHistory" (no cliff) + its [Registry] line; runtime allowed ["get"], GET /:id passes the gate (404 RECORD_NOT_FOUND), list/create/update/delete 405api-backend.route-ledger-live-parity (rev 4, P2) — PASS
pins: none (N0 forbids unit evidence) · live: :4844 stock showcase (
os dev, development posture — /api/v1/discovery environment "development"), db file:/tmp/qa3b/C4/db/ledger.db; personas admin, phone.demo (no manage_platform_settings), anonymous. Raw table /tmp/qa3b/C4/ledger/table.txt (68 rows), driver /tmp/qa3b/C4/ledger/run.sh. Ledger universe at c419609:find packages -name '*route-ledger.ts'= 16 files (/tmp/qa3b/C4/ledgers.txt); REST_ROUTE_LEDGER families = 20.{"results":[]}, POST /data/showcase_task/batch 400 VALIDATION_FAILED (handler); forms GET /forms/no_such_form 404 FORM_NOT_FOUND (handler code); search GET /search?q=qa 200; security GET /security/suggested-bindings 200; security-explain GET and POST /security/explain 200 allowed:true; record-shares GET /data/showcase_task/:id/shares 200{"data":[]}; sharing-rules GET /sharing/rules 200; user-work GET /security/users//assigned-work 200; approvals GET /approvals/requests 200; email POST /email/send {} 400 VALIDATION_FAILED "subject is required"; analytics POST /analytics/dataset/query {} 400 VALIDATION_FAILED; packages GET /packages 200, POST /packages/publish {} 400 MISSING_REQUIRED_FIELD; external-datasource GET /datasources/showcase_external/external/tables 200 (customers, orders){"success":false,"error":{"code":"ENDPOINT_NOT_FOUND","message":"Not found"}}; GET /api/v1/reports → the identical body (retired family); every handler 404 in the sweep carries its own code (FORM_NOT_FOUND, FILE_NOT_FOUND, RECORD_NOT_FOUND, NOT_FOUND "Object not found"), none byte-equal to the control{"openapi":"3.1.0","info":{"title":"ObjectStack REST API","version":"18.3.0",…{"success":true,"data":[]}; POST /keys {name} 201; GET /notifications 200; GET /security/suggested-bindings 200; GET /i18n/locales 200 (en, zh-CN); POST /analytics/query {} 400 VALIDATION_FAILED "Invalid AnalyticsQuery body: cube: …" (capability on){"success":true,"data":{"manifests":[…(ledger packages/services/service-settings/src/settings-route-ledger.ts); GET /api/v1/datasources (admin) 200 [default, showcase_external] (ledger packages/services/service-datasource/src/datasource-route-ledger.ts). The retired unledgered-mount finding is not filedmanage_platform_settingscapability."; admin 200 driver catalog; GET /api/v1/datasources anonymous 401; both refusal bodies differ from the ENDPOINT_NOT_FOUND control{"ok":true,"listeners":0,"reason":"manual-trigger"}; trigger-api POST /api/v1/automation/hooks/no_such_flow/bogus → 401{"code":"INVALID_SIGNATURE","message":"Signature verification failed."}(structured, anonymous and admin alike); GET /api/v1/runtime/config anonymous 200; GET /api/v1/cloud-connection/status 200 bound:false; GET /_console → 302, GET /_console/ → 503 text/plain "The ObjectStack Console is not built…" (no Console build in this tree){"from":"in_review","next":["done","in_progress"]}= task.object.ts task_status_flow; ?from omitted → from:null, next:null; no-FSM field state/priority?from=high → next:null; not_a_real_object/state/status → 404 NOT_FOUND "Object not found" (handler body ≠ control)cli.migrate-meta-codemod (rev 7, cli) — PASS
pins: none · live: CLI only, scratch copies
/tmp/qa3b/C4/ws/examples/app-{plant,plant4,write,write3}(copies of examples/app-showcase,ws/packages+ws/node_modulessymlinked to the tree), outputs/tmp/qa3b/C4/mig/(planting patches plant.patch / plant4-extra.patch there)Registries (step 0): retired-defs 249 · retired-keys 307 · semantic 582 entry files. House sentence (step 1):
os migrate meta --from 17×223 and--from 16×69 in spec .ts sources (+ one--from 98in a test, 17 placeholder--from <N>forms; shared/retired-key.ts:57/188). Planted as raw literals (step-18 entries, so--from 17): data/Object:externalSharingModel and data/CurrencyConfig:precision (plain-literal field qa_amount) in semantic-zoo.object.ts, ui/ListView:pageName on business-unit org_chart. Each planted key's own load refusal prescribesRun \os migrate meta --from 17`` (9 such lines in the run's load output).Applied 5 mechanical change(s):listsviews[3].listViews.org_chart.pageName: pageName → (removed) (view-page-mount-removed),objects[18].fields.qa_amount.currencyConfig.precision: precision → (removed) (currency-config-precision-removed),objects[18].externalSharingModel: externalSharingModel → (removed) (object-external-sharing-model-removed)+ 2 stock flow-decision-mode-inclusive-explicit (flows[6]/[7]); exit 0--outrun and the--jsonrun: diff empty; the only new path is/tmp/qa3b/C4/mig/snap.json(outside the tree)✓ Migrated stack is schema-validagrees with--jsonschemaValid:true, applied 5. Warning arm on app-plant4 (an aged-outstripedplanted):⚠ Migrated stack does not yet pass schema validation — 1 refusal left after the chain. Resolve them, then run \os validate`:+✗ views.3.listViews.org_chart: Unrecognized key(s) on this list view: `striped`…`, agreeing with its --json schemaValid:false--from 17→ noAppliedblock,✓ Migrated stack is schema-valid, 505 advisories, exit 0; two identical planted-tree runs diff empty after the(Nms)normalization (only 774ms vs 772ms differed);--from 18→⚠ No migration step exists for protocol 18 → 18, so this run replayed nothing — that is not a finding that the metadata is canonical., noNothing to migrate--from 15→✗ Cannot migrate from protocol 15: the chain's support floor is 16 (ADR-0087 D3). Upgrade to protocol 16 by another path first, then re-run.exit 1;--jsonstdout {"error":"unsupported_from_major",…} exit 1505 manual change(s) require your judgment:block with why:/verify: lines; the stock flow edits carry↳ review the 2 edits above against the manual change [protocol 18] flow.nodes[].config.mode …; --write wrote none of them (diff)meta --from 17 --jsonexit 0 (schemaValid true, applied 5);--write --jsonexit 0--help:--write Rewrite the authored source files in place for each mechanical change traced to one literal in one project file, and the manifest's declared protocol range when the load would still refuse it; every other change is listed …; app-write:Wrote 5 of 5 mechanical change(s) into 3 file(s):(:1778/:293 flows/index.ts, :37/:29 semantic-zoo.object.ts, :28 business-unit.view.ts),✓ Re-ran the chain over the written sources: no mechanical change remains., exit 0, no range block (showcase declares ^18);diff -rvs the byte copy = exactly those 5 sites in those 3 files; re-run without --write lists no Applied, advisory block identical to the dry run's; second --write →ℹ --write: the chain made no mechanical change here, so no file was written.; app-write3--write --jsonexit 0, write.status 'written', write.written[] = 5 {conversionId, path, file, line}. Refusal arm on app-plant4:Wrote 2 of 3 mechanical change(s) into 1 file(s)+1 mechanical change(s) left for you to apply by hand: … not written [helper]: the value is built by \Field.currency(…)`, not written as a literal,✓ Re-ran the chain over the written sources: only the 1 change(s) left above remain.` (kind from the closed set — the contract working); its written zoo file is byte-equal to app-write's(removed); the written tree is schema-valid--json→ exit 1missing_from_major;--from abc→ exit 2 "INVOCATION ERROR — Parsing --from; Expected an integer … The command never ran";--from 17 --to 16→ exit 0⚠ --from 17 is above --to 16, so the range is inverted and this run replayed nothing — that is not a finding…(honest);--from 17 --to 19→ exit 0,Chain: protocol 17 → 19 (this runtime implements protocol 18)with the 5 edits and no word that 18 → 19 has no step (parked at 8e3d4a5, unchanged). No findings.integration-system.connector-declarative-boot (rev 6) — PASS
pins: packages/qa/dogfood test/showcase-declarative-mcp.dogfood.test.ts → 2/0 (HTTP: in-process booted showcase via bootShowcase, requests through stack.apiAs; negative[0] only scores, the rest corroborates halves; run alone, log /tmp/qa3b/C4/pins/dogfood.log) · packages/spec src/integration/connector-provider.test.ts → 15/0 (SPEC UNIT, schema parse — below A3/N1's build layer; 2-file spec run 47/0) · packages/services/service-automation src/connector-materialization.test.ts → 110/0 (in-process LiteKernel over a fake provider — below A4/A5's served-boot layer) · live: scratch copy
/tmp/qa3b/C4/ws/examples/app-conn(showcase copy, ws/packages + ws/node_modules symlinked), defaultos devcompile under OS_PORT=4846,--log-level info --no-watch, db /tmp/qa3b/C4/db/conn.db (case d: conn-d.db). Its connectors index carries env-selected scratch cases (QA_CASE, QA_DISABLE_STATUS — the L3 recipe file, echo base repointed to my own :4849, never dialed). Logs /tmp/qa3b/C4/conn/{server,server-d,server-dis2}.log, compile-{a,b,c,d}.log.Self-ping check (RUNNER env fact): the ping's
ping.body.data.uptime20.524 s vs this boot's own GET /health uptime 20.595 s read 71 ms later — the dispatch landed on this boot.showcase_status_api origin=declarative state=ready; boot logINFO Connector registered: showcase_status_api (1 action handlers, origin: declarative)origin=declarative state=ready+ its boot lineorigin=declarative state=ready+ its boot line/api/v1/health get getHealth)ping.body= {success, data:{status ok, version 18.3.0, uptime 20.52}} from this boot; …/showcase_mcp_connector_echo/trigger → 200 outputecho.structuredContent {"upper":"OBJECTSTACK"}; GET /automation/:name/runs → bothcompletedos compileon the scratch copy, each exit 2: (a)✗ connectors.4.providerConfig: \providerConfig` requires a `provider` — a connector entry with no provider is a catalog descriptor (ADR-0097).(b)✗ connectors.4.actions: Provider-bound connector instance 'qa_neg_b' must not author `actions` — the 'rest' provider derives them from the upstream at boot (ADR-0097 §5).(c)✗ connectors.4.authentication: Provider-bound connector instance 'qa_neg_c' must not inline secrets via `authentication`; reference credentials with `auth: { type, credentialRef }` instead (ADR-0097 §3).`✗ Plugin com.objectstack.service-automation failed to start - rollback complete: [Automation] connector instance 'qa_neg_d' declares provider 'no_such_provider', but no provider factory is registered. … Installed providers: [openapi, mcp, rest] (ADR-0097).; the process exited, /health 000, no process leftenabled:false: listing total 4 [showcase_status_openapi, showcase_mcp_tools, rest, slack] without it, 0Connector registered: showcase_status_apilines; ping dispatch → 400 FLOW_FAILED "no handler for 'showcase_status_api.request' — is the connector plugin registered?"; GET /meta/connector/showcase_status_api still serves it with enabled:false. Torn-down-on-reload is pinned in-process only (service-automation pin), not driven liveintegration-system.connector-degraded-recovery (rev 7) — PASS
pins: none · live: port 4847 · broken-upstream copy via
node scripts/qa/qa-fixture.mjs broken-upstream-app --dir /tmp/qa3b/C4/brk(missing-script variant; db /tmp/qa3b/C4/db/conn-broken.db) and--variant config-fault --dir /tmp/qa3b/C4/cfg(db conn-cfgfault.db), each compiled in the copy under OS_PORT=4847 (os devdefault compile,--log-level info --no-watch, no--ui), both torn down after (teardown --dir, exit 0). Logs /tmp/qa3b/C4/brk/server.log, /tmp/qa3b/C4/cfg/server.log (copies removed by teardown; the excerpts below are the record)qa-fixture restore-upstreamat 10:45:04 (file re-added, no config change); serve PID 13632 (started 10:44:07) unchanged throughout; 10:46:19.533 "INFO [Automation] recovered connector instance 'showcase_mcp_tools' via provider 'mcp' (1 action(s))" (the scheduled 80 s retry); descriptor state 'ready', actions [echo_upper], no degradedReason; trigger → 200 in 0.031 s, output {"echo.structuredContent":{"upper":"OBJECTSTACK"}}. (TheConnector 'showcase_mcp_tools' replacedWARN printed on every failed retry too — not read as the signal, per the step)records-forms.named-import-mapping (rev 1, P2) — PASS
pins: none · live: port 4845, db file:/tmp/qa3b/C4/db/import.db · admin (+ Mei Phone for a probe) · trace /tmp/qa3b/C4/import/trace.txt
Full Name,E-mail,Company,Channel,Message+ 2 rows} → 200 {writeMode:"upsert", total 2, ok 2, created 2, updated 0}; read-back: {name "Ada C4imp", email ada.c4-imp@example.com, company Acme, source "website" (Webform mapped), message "hello one"} and {name "Bo C4imp", …, source "referral" (Partner Referral mapped)}Fails
None. No clause failed in this group; no product-defect card to draft. Two known open findings were RE-MEASURED as asked and still reproduce at c419609 (not re-drafted): card-B-api-2 (private plain-HTTP AS metadata advertises an https issuer while the PRM and endpoints say http — ai.mcp-oauth-private-host-transport probe) and card-api-4 (a deployment-authored action on a packaged object listed twice by list_actions with a refusedFaces entry — ai.mcp-run-action-exposure-gate probe). The stdio get_record uncoded not-found text (ee9ad83 L2 note) also still reproduces.
Parked
*route-ledger.ts;find packages -name '*route-ledger.ts'at c419609 lists SIXTEEN (/tmp/qa3b/C4/ledgers.txt; the five unnamed: plugin-audit comment-reaction, plugin-auth personal-view, plugin-sharing sharing, service-messaging messaging, service-print print — each answered non-routing-404 live). REST_ROUTE_LEDGER has 20 families (recycle-bin, user-work beyond steps[0]'s 18). The probe rowPOST /data/:object/actions/:nameis correctly a routing 404 (rest-route-ledger.ts:343 says it is not a route) — a next run should not sample it as a ledger row.mode; the run lists TWO flow-decision-mode-inclusive-explicit edits on the stock flows (flows[6] any_found, flows[7] needs_exec).--from 17 --to 19still prints no word that 18 → 19 has no step (parked at 8e3d4a5).cd APP && setsid cmd &boot line records the subshell pid, not the session leader, so akill -- -PIDstop silently misses the server; record$!of a baresetsid … &. (My own driving slip, fixed mid-run; no verdict rests on it.)objectstack-fleet commentedon Nov 13, 2026
qa#3 · os-tesla · session_01L9qibw6yXcLi9aRRBpPT4W
Close-out 2026-11-13T10:54Z.
Extracted: #33083 · ai.mcp-oauth-private-host-transport · observed beside the clauses
Extracted: #33029 · ai.mcp-run-action-exposure-gate · observed beside the clauses
Extracted: #33030 · ai.mcp-coded-refusals-and-runtime-exposure · observed beside the clauses
Parked-on: #32908 · checklist-accuracy notes and fixture gaps of this slice
objectstack-fleet commentedon Nov 13, 2026
qa#3 · os-tesla · session_01L9qibw6yXcLi9aRRBpPT4W
Closed 2026-11-13T10:55Z: the c419609 api delta is judged; close-out above.