You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
The import template (GET /data/:object/export?template=true) sits behind the EXPORT gate (allowExport): a caller who may import but not export cannot download it — gate it by the import door instead? (from #18386 acceptance-6 verification) #20896
Filed by the domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1, seat post #19357), from the #18386 acceptance-6 machine verification (5914688255, risk 2). ⛔ Not a claim. The decision analysis is the first comment.
What the tree does (read on main00a92e18da)
packages/rest/src/rest-server.ts:9633 and :9636 run the export door's two gates, enforceApiAccess(..., 'export') and enforceExportPermission. They run BEFORE the template=true branch at :9667–:9672, so a template download needs allowExport on the object.
The import door has its own gates: enforceApiAccess(..., 'import') at :9093 and :9200, plus the write path's create permission.
allowExport has been a deliberate segregation-of-duties axis since protocol 17 (17.export-axis-opt-in: "Reading a record and taking a bulk machine-readable copy of the whole table are different privileges"). member_default deliberately does not carry it, and protocol 18 removed the admin wildcard (18.admin-export-wildcard-removed).
The template carries no records: only the columns the caller may write, which getWritableFields narrows by field-level security, one example row of placeholder values, and the instructions sheet.
Measured (verification 5914688255)
In examples/app-showcase, no permission set grants allowExport. An ordinary member (create and read on showcase_task) and the dev admin both get 403 EXPORT_NOT_PERMITTED for ?template=true.
#18386's body, 实现要点: 「权限沿用现有两道闸(enforceApiAccess('export') + enforceExportPermission)」. The template inherited the export gates by design of record, and this card asks whether that design holds.
Ruling: batch #259 item 3 · letter A · maintainer 「同意」 2026-09-30T22:58Z
Director seat, summon #31, session_01PGMD6TRDaZY8Ubyo7Ukm66 (GitHub os-litant; written as objectstack-fleet[bot] via the relay). Batch #259 was presented in the live director chat (four cards; this one item 3, director recommendation A, fallback C); the maintainer answered the batch with one word, 「同意」. The seat's analysis 5915133835 reached the same letter; this ruling was derived from axis ① first and the letters coincide.
The ruling
A — the import template is gated by the import door.GET /data/:object/export?template=true keeps its URL and its place in the export route's closed parameter set; the template=true branch is judged by enforceApiAccess(..., 'import') plus the caller's create permission on the object, and the two export gates (enforceApiAccess(..., 'export'), enforceExportPermission) no longer stand in front of it. The non-template export is unchanged. B (keep the export gates) and C (either gate passes) not taken. #18386's design line 「权限沿用现有两道闸(enforceApiAccess('export') + enforceExportPermission)」 is superseded for the template branch by this ruling; #18386's own ruling 5904855243 (Q1 A / Q2 C) is untouched.
Readings (this summon, origin/main013f97d)
packages/rest/src/rest-server.ts:9633enforceApiAccess(..., 'export') and :9636enforceExportPermission(...), both before the template=true branch at :9663; the import door's enforceApiAccess(..., 'import') at :9093 / :9200 and its writeMode variant at :9118 / :9217 — verified.
packages/rest/src/import-template.ts:5 (an xlsx with no data rows), :182-194 (getWritableFields narrows the columns) — verified.
examples/app-showcase/src/security/ carries no allowExport (0 files) — verified; the 403 reading itself is the seat's verification 5914688255, not re-run here.
Pins, both halves: a caller with create on the object and no allowExport gets 200 and the workbook; a caller without create gets 403 from the import door and the template builder was never called; a caller with allowExport and no create gets 403 on template=true; the non-template export, with and without allowExport, answers exactly as before (a refusal pin and a preservation pin).
Text that follows:feat(rest): 导出接口新增 ?template=true —— 输出只含「可填列」的 xlsx 导入模板 #18386's body line 94 is corrected in the same stroke by the claiming seat, quoting this ruling; the export door's docblocks at rest-server.ts:644 and :10040, and any REST doc sentence that says the template needs the export permission; objectui#9600 gets a pointer that the button needs the import (create) permission, not export.
Fork clause: the premise is that the template's columns depend on getWritableFields only. If the import door's writeMode check turns out to decide the column set, the dev reports the fork on this card rather than picking a mode.
Claim: PM loop round 32 · 2026-09-30T23:21Z
Session: session_01Sfe5YjBLwB9J3y8fvm2xq1
Account: os-justin (the seat's linked user as GET /user answers it; the card's assignee from this act)
Branch: claude/issue-20896-template-import-door
Worktree: objectstack-issue-20896
Domain: domain:spec (ruling A 5921162178, execution parameters)
Seat: domain:spec#5 (seat post #19357)
Clause-②: no (no schema or published-export change; the route and its closed parameter set are unchanged, as the ruling states)
Scope: ruling A's execution parameters, taken whole. Level S.
In packages/rest/src/rest-server.ts, the template=true branch of GET /data/:object/export (≈:9663) is judged by enforceApiAccess(..., 'import') plus the caller's create permission on the object. The two export gates (enforceApiAccess(..., 'export') ≈:9633, enforceExportPermission ≈:9636) no longer stand in front of it. The non-template export is unchanged.
The pins cover both halves:
create on the object and no allowExport → 200 and the workbook;
no create → 403 from the import door, and the template builder is never called;
allowExport and no create → 403 on template=true;
the non-template export, with and without allowExport, answers exactly as before.
Each pin is ablated.
The text follows the ruling: the export door's docblocks (≈:644, ≈:10040), and any REST doc sentence that says the template needs the export permission.
⚠️Fork clause: if the import door's writeMode check turns out to decide the template's column set (today getWritableFields narrows it, import-template.ts ≈:182-194), the dev reports the fork on this card and does not pick a mode.
File surface (stop on breach and explain it in the report): rest-server.ts and packages/rest tests; REST docs under content/docs/, text only; .changeset/.
Text the seat does itself:
objectui#9600 gets its pointer (the button needs create, not export) when this PR lands.
Container & model: S, mode:subagent, model: opus.
Review: an at-tier contract review is owed before enqueue.
Thread-read: 5921162178
Serial constraints cleared: read at this stamp.
{
"issue": 20896,
"status": "done",
"branch": "claude/issue-20896-template-import-door",
"pr": "https://github.com/objectstack-ai/objectstack/pull/20977",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (subagent run; the parent session's id)",
"premise_still_valid": true,
"summary": "Ruling A is in: on GET /data/:object/export, a request whose template value reads true (read by readTemplateMode on the template key alone) goes through the new enforceImportTemplateGates instead of the export gates. Those gates are the import door's own stage-1 call enforceApiAccess(..., 'import') (404/405), plus the caller's create permission, asked of the security service as explain({ object, operation: 'create' }). allowed false or a throw answers 403 PERMISSION_DENIED in the shared sendError envelope. No security service, or one without explain, passes, as enforceExportPermission does. Every non-template request goes through the export gates exactly as before. One dispatch assumption did not hold: the import door has no route-level check of the caller's create permission to reuse. Its route gates (stage 1 and the stage-2 writeMode gate) are object-level only. The caller's create is judged by the engine's security middleware on each written row (a PERMISSION_DENIED row inside a 200 report). So, following the ruling's intent, the template branch reuses stage 1 verbatim and asks the contract's non-optional explain for the middleware's verdict, with no re-derivation from permission sets; runtime mayReadRunState is the precedent. Fork clause NOT triggered: templateColumns takes no write mode, and getWritableFields(object, context) has no operation parameter; stage 2 is not asked because a template request names no mode. Text changes: docblocks at DATA_EXPORT_PARAMS and answerImportTemplate, docs/permissions/permission-sets.mdx, a new patch changeset, and one sentence corrected in the still-pending .changeset/18386-export-import-template.md, which this change made false. Clause-② no holds: the template has not been released yet (the 18386 changeset is still pending), so no published accept set narrows.",
"tests": "All at head 501dca7347 (code blob rest-server.ts 8e09988a9310) unless noted; every heavy run through os-verify-lock, VERDICT lines quoted. (1) Build of the rest dependency closure: turbo build --filter='@objectstack/rest^...' --concurrency=2, 24/24 tasks, VERDICT command-exit 0, rebuilt after the main merge 86cb4bb146 (24/24, exit 0). (2) pnpm --filter @objectstack/rest typecheck: VERDICT command-exit 0 (tsc --noEmit plus check:test-typecheck OK, 0 debt). The test file is in tsconfig.test.json's program (tsc --listFilesOnly: 1 hit). (3) Full package suite, pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: Test Files 248 passed (248), Tests 4962 passed, 114 skipped (5076), VERDICT command-exit 0. Earlier heads also green: 3c7d7dd4ef 245 files / 4891 passed; 86cb4bb146 248 / 4962. (4) import-template-route + rest-export-permission-gate + rest-server-closed-query-params + import-template: 4 files, 128 passed, exit 0. (5) Ablations, each via scripts/ablation-replace.mjs in wrap mode (anchor 1 -> 0, blob changed, then the restore proven: blob == HEAD 8e09988a9310 and git diff HEAD empty, plus an outer trap that compares hashes). The subject is imported as ./rest-server (src, relative), so no dist rebuild is involved. A1, the template branch put back behind the export gates (`if (false)`): 6 failed / 31 passed. Red: create+noExport 200, noCreate 403+builder never called, allowExport+noCreate 403, throw->403, create-only object serves, list-only object 405. Green: preservation. A2, caller create gate deleted: 4 failed / 33. Red: the 200 pin (via its explain-was-asked assertion), noCreate 403, allowExport+noCreate 403, throw->403. A3, every request routed through the import gates (`if (true)`): 2 failed / 35. Red: both preservation pins (no-allowExport export 403 EXPORT_NOT_PERMITTED with no row read; allowExport+noCreate export keeps pre-change headers/text/sha256 and never asks explain). A5, the object half judged by export exposure: 2 failed / 35. Red: both object-half pins. All four were also run once at 617255a015 with the same verdict counts, before the envelope fix. (6) node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: exit 0, 93 commands for the 5 changed paths. All 93 were run at 501dca7347, each exit captured before any pipe. 89 exited 0 on the first pass. check:type-check-debt and check:skill-examples exited 3 PREREQUISITE NOT MET; after building their closure (turbo build of rest, runtime, driver-turso, plugin-auth, service-cluster, service-job, service-messaging and client-react: 35/35, exit 0) both re-ran to exit 0. check-empty-changeset --base origin/main exit 1: the DELIBERATE CORRECTION class (this PR edits the pending 18386 changeset); it stays red by design and needs a written confirmation on the PR. check:dual-build-cjs-loads exit 3: NOT MEASURED, reason: it reads dist of about 44 packages (apps, adapters, client, ...), which only a full build provides; CI builds them. Reconciliation dispatch-gates --ran with recorded exit codes: exit 0, 93 accounted, 92 run, 1 NOT-MEASURED. check:route-envelope first went red (siblingCode 59 vs 58) on the flat 403 body; the body now goes through sendError and the gate is exit 0. (7) Narrowed eslint: pnpm exec eslint --no-inline-config --format json on rest-server.ts + import-template-route.test.ts, exit 0, 2 files in the json, 0 errors, 0 warnings, at 501dca7347. Population from eslint.config.mjs: the files glob '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' (line 971) covers both, and .md/.mdx sit in no eslint glob. Invariance: the config never enables type-aware linting (its own text at :327-328: no parserOptions.project), and no export changed, so no untouched file's verdict can move. (8) PR CI at 501dca7347, read once with no waiting: all seven required contexts completed success; Check Changeset failure is the expected deliberate-correction red (not a required context).",
"mcp_calls": "0",
"api_writes": "3 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft), run 36793595529, read-back 6360 bytes identical; (2) label-write --assign os-justin, POST /repos/objectstack-ai/objectstack/issues/20977/assignees, run 36793656255, read-back matches; (3) this os-dev-report, POST /repos/objectstack-ai/objectstack/issues/20896/comments via post-stamped.mjs. An earlier pr_create attempt was refused by write-pace (exit 10, budget 40/h), so nothing was written and it does not count. git push: 5 pushes to the branch (empty-branch probe, 617255a015, 3c7d7dd4ef, merge 86cb4bb146, 501dca7347; not REST).",
"open_questions": [
{
"question": "Check Changeset is red by design: this PR rewrites one sentence of the still-pending .changeset/18386-export-import-template.md. Old sentence: 'The same two permission checks as the export apply: an object that does not expose export answers 405, and a caller without the export permission answers 403.' New sentence: 'The import's permission checks apply, not the export's: an object that exposes neither create nor update answers 405, and a caller without the create permission on the object answers 403. The export permission (allowExport) is not needed.' check-empty-changeset requires the correction to be confirmed in writing on the PR. Confirm it?",
"options": [
"A: confirm on PR #20977 that the correction stays (it leaves Check Changeset red, which is not a required context, and the merge goes over it)",
"B: revert the 18386 sentence and let .changeset/20896-template-import-door.md alone describe the gate; the next release then ships a minor entry stating the export gates beside a patch entry contradicting it"
],
"recommendation": "A, because the 18386 note is unreleased and is the entry an upgrading reader will grep for the template; shipping it false and correcting it in a sibling entry of the same release is the erratum shape AGENTS.md refuses for CHANGELOG text."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed — the create verdict is composed from ISecurityService.explain because the contract has no dedicated create/write twin of canReadObject; explain denies a caller whose permission sets resolve EMPTY where the middleware skips its CRUD gate (closed direction, reachable only with no baseline set). Recorded in the PR's Acceptance notes and in the enforceImportTemplateGates docblock.",
"carrier: seat (PR #20977 body) · noted, not filed — the body's 'Verification at opening' names head 3c7d7dd4ef; the head is now 501dca7347 (main merged as 86cb4bb146, then the refusal moved to the shared envelope). Suggested seat edit, one paragraph under Verification: 'At 501dca7347: the 403 answers { success: false, error: { code: PERMISSION_DENIED, message, details: { object } } } through sendError (check:route-envelope refused the flat form); rest typecheck exit 0; rest suite 248 files / 4962 passed exit 0; ablations A1-A3, A5 red as expected and restored; dispatch-gates 93 accounted, 92 run, 1 NOT MEASURED (dual-build-cjs-loads).'"
]
}
The premise the dispatch assumed did not hold, and the deviation is accepted pending review. The import door has no route-level check of the caller's create permission. Its stage-1 and writeMode gates are object-level, and create is judged per written row by the engine's middleware. So the template branch reuses stage 1 (enforceApiAccess(..., 'import')) verbatim and asks the contract's own explain({ object, operation: 'create' }) for the create verdict, with no re-derivation from permission sets. The at-tier review is asked to judge that the verdict matches ruling A's "the caller's create permission on the object". It is also asked about the noted divergence: explain denies a caller whose permission sets resolve empty where the middleware skips its CRUD gate, which is the closed direction.
Fork clause: not triggered. templateColumns takes no write mode.
Q1 (the correction to the still-pending .changeset/18386-export-import-template.md): the confirmation check-empty-changeset asks for is a person's (scripts/check-empty-changeset.mjs:605-612), not the seat's. After the review, the seat puts it to the maintainer on this PR with a decision comment and needs-user-decision, the same path as PR docs(changeset): correct two scope sentences in the pending service-analytics masked-field note #20991.
PR body: the seat adds the current-head verification paragraph the report drafted, in this act.
Next:needs:contract-review goes on the PR and this card, then the at-tier review. objectui#9600's pointer is posted at landing.
{
"issue": 20896,
"status": "done",
"branch": "claude/issue-20896-template-import-door",
"pr": "https://github.com/objectstack-ai/objectstack/pull/20977",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (subagent run; the parent session's id)",
"premise_still_valid": true,
"summary": "Patch round after the maintainer chose B on decision 5922804275, per the seat's comment 5923146385 on PR #20977. New head 8e2d1fda64, one appended commit with no rebase, amend or force-push. It restores .changeset/18386-export-import-template.md from the merge base 525b8139a2. git diff of that file between the merge base and HEAD is empty (0 bytes), and the file's blob 542d0899944e equals both the merge base's and origin/main's. Everything else is unchanged: the template import-door change, its pins, the docs line and .changeset/20896-template-import-door.md. That changeset never referred to the 18386 correction (no mention of 18386, pending notes or corrections), so nothing was cut. main was not merged: no conflict, and the 12 commits since are CI's joint check on the merge ref. They touch other files of packages/rest (error-response.ts, tests, tsconfig.json), but not this diff's files. PR body untouched, as instructed.",
"tests": "At 8e2d1fda64. node scripts/check-empty-changeset.mjs --base origin/main: exit 0 ('No changeset from the merge base modified or deleted by this diff'), and its --self-test exit 0. check-changeset-no-major --base origin/main: exit 0. check-adr-0087-registration --base origin/main: exit 0. pnpm --filter @objectstack/rest typecheck: VERDICT command-exit 0. Full rest suite (pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2): Test Files 248 passed (248), Tests 4962 passed, 114 skipped (5076), VERDICT command-exit 0. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: exit 0, 93 commands for 4 changed paths. It warns that the derivation tree is 12 commits behind origin/main, with 7 family files changed upstream. All 93 were run with exit codes captured before any pipe: 92 exit 0. check:type-check-debt and check:skill-examples first exited 3 (PREREQUISITE NOT MET in the recreated worktree) and exited 0 after their closure was built (35/35 cached). check:dual-build-cjs-loads exit 3: NOT MEASURED, because it needs the dist of about 44 packages; CI builds them. dispatch-gates --ran with exit codes: exit 0, 93 accounted, 92 run, 1 NOT-MEASURED. check:nul-bytes exit 0. The code is identical to 501dca7347, so the ablations reported on that head (A1-A3, A5) still apply unchanged.",
"mcp_calls": "0",
"api_writes": "1 relay write: this os-dev-report, POST /repos/objectstack-ai/objectstack/issues/20896/comments via post-stamped.mjs (relay dispatch POST /repos/objectstack-ai/objectstack/dispatches). git push: 1 (8e2d1fda64, not REST).",
"open_questions": [],
"out_of_scope_findings": []
}
Landed: PR #20977 → 8f784959cf, and the card is done
domain:spec seat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-10-01. Holder of claim 5921450654.
Landed: PR fix(rest): the import template answers to the import door's gates, not the export's (#20896) #20977 went through the merge queue as 8f784959cf, with one parent (bb2eccf7ed, read with git rev-list --parents). All 4 files have the same per-file git patch-id --stable as the reviewed head 8e2d1fda64 against the merge base 525b8139a2. Review records: 5922797800, then the delta PASS 5923454406.
Delivered (ruling A):
GET /data/:object/export?template=true is judged by the import door's own enforceApiAccess(..., 'import') plus the caller's create verdict from the security contract's explain.
The export gates no longer stand in front of it.
The non-template export is unchanged.
The maintainer's B on the 18386 note:.changeset/18386-export-import-template.md ships as written, beside this PR's patch entry.
Closed by Closes #20896 at merge. The seat now removes pm:dispatched and the assignee. The objectui#9600 pointer is posted in the same act.
Activity
objectstack-fleet commentedon Sep 30, 2026
os-decision-facets
决策请求:导入模板的下载,该挂在「导出」权限后面,还是「导入」权限后面? · 2026-09-30T16:11Z
domain:specseat 5(session_01Sfe5YjBLwB9J3y8fvm2xq1)。#18386 验收 6 的机器验证测到:showcase 里没有一个人能下载导入模板。原因是模板接口沿用了导出的两道闸,而导出权限自 spec 17 起默认不给。这张卡请你定模板该跟哪道闸走。一句话问题
模板里一条数据都没有,只有这个人能写的列和填写说明。现在却要求他有「整表导出」的权限才能下载,于是只能导入、不能导出的人拿不到模板。
Governing text
enforceApiAccess('export')+enforceExportPermission)」。export-axis-opt-in:读记录与批量复制整表是两种不同的特权(Salesforce「Export Reports」、Dynamics「Export to Excel」同样分开);member_default有意不带导出权限。spec 18admin-export-wildcard-removed又去掉了管理员的通配导出。前提(每条带复查方法)
rest-server.ts:9633、:9636两道导出闸都在:9672的模板分支之前。5914688255第 1 步;examples/app-showcase/src/security/permission-sets.ts里没有allowExport。import-template.ts,数据行为 0,列经getWritableFields按字段级权限收窄。选项与代价
enforceApiAccess('import')加该对象的新建权限;URL 不变allowExport业务直译:A = 模板属于「导入」这件事;B = 模板属于「导出」这件事;C = 两边都算。
四轴(业务立场)
推荐
A。 只看①选 A;②③④ 是否翻转:否。回退:C。
置信缺口:「导入闸」具体用
enforceApiAccess('import')加对象新建权限,还是复用导入接口现有的整套检查顺序,要等实现时逐条对照导入接口来定。自检
裁后执行
rest-server.ts的模板分支):模板改走导入闸。补钉子:有新建权限、没有导出权限的人能拿到模板;没有新建权限的人拿到 403;不带template的导出行为一字不变。allowExport,并在 objectui#9600 注明按钮需要导出权限。四棱
① 项目长远合理性:A 与主流一致,守住导出的职责隔离。
② 实际业务拉动:模板按钮就在导入向导里。
③ 防 AI 犯错 / 安全:模板没有数据,列已按字段级权限收窄。
④ 创业阶段不扩散:A 只改一个分支的闸。
Prior rulings read: allowExport, export gate, import template → #18386 正文与裁定
5904855243;spec 17export-axis-opt-in、spec 18admin-export-wildcard-removed;thread: 本卡无评论。推荐:A。只看①选 A;②③④ 是否翻转:否。置信缺口:见上。
Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Ruling: batch #259 item 3 · letter A · maintainer 「同意」 2026-09-30T22:58Z
Director seat, summon #31,
session_01PGMD6TRDaZY8Ubyo7Ukm66(GitHubos-litant; written asobjectstack-fleet[bot]via the relay). Batch #259 was presented in the live director chat (four cards; this one item 3, director recommendation A, fallback C); the maintainer answered the batch with one word, 「同意」. The seat's analysis 5915133835 reached the same letter; this ruling was derived from axis ① first and the letters coincide.The ruling
A — the import template is gated by the import door.
GET /data/:object/export?template=truekeeps its URL and its place in the export route's closed parameter set; thetemplate=truebranch is judged byenforceApiAccess(..., 'import')plus the caller's create permission on the object, and the two export gates (enforceApiAccess(..., 'export'),enforceExportPermission) no longer stand in front of it. The non-template export is unchanged. B (keep the export gates) and C (either gate passes) not taken. #18386's design line 「权限沿用现有两道闸(enforceApiAccess('export')+enforceExportPermission)」 is superseded for the template branch by this ruling; #18386's own ruling 5904855243 (Q1 A / Q2 C) is untouched.Readings (this summon,
origin/main013f97d)packages/rest/src/rest-server.ts:9633enforceApiAccess(..., 'export')and:9636enforceExportPermission(...), both before thetemplate=truebranch at:9663; the import door'senforceApiAccess(..., 'import')at:9093/:9200and itswriteModevariant at:9118/:9217— verified.packages/rest/src/import-template.ts:5(an xlsx with no data rows),:182-194(getWritableFieldsnarrows the columns) — verified.examples/app-showcase/src/security/carries noallowExport(0 files) — verified; the 403 reading itself is the seat's verification 5914688255, not re-run here.pm:blocked— verified.allowExport|template=true|import template|export-axis|admin-export-wildcardoverAGENTS.md docs/adr packages/spec/src→ 54 hits; deciding: spec 17export-axis-opt-inand spec 18admin-export-wildcard-removed(the export axis segregates a bulk copy of data; a template carries none), ADR-0104:919, feat(rest): 导出接口新增 ?template=true —— 输出只含「可填列」的 xlsx 导入模板 #18386 ruling 5904855243; thread: 5915133835.四棱(本裁决新记录)
Execution parameters (ruled here; no further decision card)
pm:queue,domain:spec(unchanged), level S,mode:subagent,Clause-②: no(no schema or published-export change; the route and its closed parameter set are unchanged). Same file as feat(rest): 导出接口新增 ?template=true —— 输出只含「可填列」的 xlsx 导入模板 #18386's round 2 (PR feat(rest): GET /data/:object/export?template=true answers an xlsx import template (#18386) #20683, in flight): serial — claim after PR feat(rest): GET /data/:object/export?template=true answers an xlsx import template (#18386) #20683 is onmain, and name it underSerial constraints cleared.allowExportgets 200 and the workbook; a caller without create gets 403 from the import door and the template builder was never called; a caller withallowExportand no create gets 403 ontemplate=true; the non-template export, with and withoutallowExport, answers exactly as before (a refusal pin and a preservation pin).rest-server.ts:644and:10040, and any REST doc sentence that says the template needs the export permission; objectui#9600 gets a pointer that the button needs the import (create) permission, not export.getWritableFieldsonly. If the import door'swriteModecheck turns out to decide the column set, the dev reports the fork on this card rather than picking a mode.Generated by Claude Code
objectstack-fleet commentedon Sep 30, 2026
Claim: PM loop round 32 · 2026-09-30T23:21Z
Session:
session_01Sfe5YjBLwB9J3y8fvm2xq1Account:
os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)Branch:
claude/issue-20896-template-import-doorWorktree:
objectstack-issue-20896Domain:
domain:spec(ruling A5921162178, execution parameters)Seat:
domain:spec#5(seat post #19357)Clause-②: no (no schema or published-export change; the route and its closed parameter set are unchanged, as the ruling states)
Scope: ruling A's execution parameters, taken whole. Level S.
packages/rest/src/rest-server.ts, thetemplate=truebranch ofGET /data/:object/export(≈:9663) is judged byenforceApiAccess(..., 'import')plus the caller's create permission on the object. The two export gates (enforceApiAccess(..., 'export')≈:9633,enforceExportPermission≈:9636) no longer stand in front of it. The non-template export is unchanged.allowExport→ 200 and the workbook;allowExportand no create → 403 ontemplate=true;allowExport, answers exactly as before.:644, ≈:10040), and any REST doc sentence that says the template needs the export permission.writeModecheck turns out to decide the template's column set (todaygetWritableFieldsnarrows it,import-template.ts≈:182-194), the dev reports the fork on this card and does not pick a mode.File surface (stop on breach and explain it in the report):
rest-server.tsandpackages/resttests; REST docs undercontent/docs/, text only;.changeset/.Text the seat does itself:
issue_patch, read back identical).Container & model:
S,mode:subagent,model: opus.Review: an at-tier contract review is owed before enqueue.
Thread-read: 5921162178
Serial constraints cleared: read at this stamp.
mainase5c7d07ed4.packages/rest/src/rest-server.ts,import-template.ts, or the import/export REST docs. PR fix(security,service-analytics)!: the security contract publishes which fields a caller may query on, and the analytics field gate refuses a masked field as a group or filter member (#20935) #20955 adds only apackages/resttest file of its own.Generated by Claude Code
fix(rest): the import template answers to the import door's gates, no…
docs(rest): the template's changeset and permission docs name the imp…
fix(rest): the template's create refusal answers in the shared error …
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{ "issue": 20896, "status": "done", "branch": "claude/issue-20896-template-import-door", "pr": "https://github.com/objectstack-ai/objectstack/pull/20977", "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (subagent run; the parent session's id)", "premise_still_valid": true, "summary": "Ruling A is in: on GET /data/:object/export, a request whose template value reads true (read by readTemplateMode on the template key alone) goes through the new enforceImportTemplateGates instead of the export gates. Those gates are the import door's own stage-1 call enforceApiAccess(..., 'import') (404/405), plus the caller's create permission, asked of the security service as explain({ object, operation: 'create' }). allowed false or a throw answers 403 PERMISSION_DENIED in the shared sendError envelope. No security service, or one without explain, passes, as enforceExportPermission does. Every non-template request goes through the export gates exactly as before. One dispatch assumption did not hold: the import door has no route-level check of the caller's create permission to reuse. Its route gates (stage 1 and the stage-2 writeMode gate) are object-level only. The caller's create is judged by the engine's security middleware on each written row (a PERMISSION_DENIED row inside a 200 report). So, following the ruling's intent, the template branch reuses stage 1 verbatim and asks the contract's non-optional explain for the middleware's verdict, with no re-derivation from permission sets; runtime mayReadRunState is the precedent. Fork clause NOT triggered: templateColumns takes no write mode, and getWritableFields(object, context) has no operation parameter; stage 2 is not asked because a template request names no mode. Text changes: docblocks at DATA_EXPORT_PARAMS and answerImportTemplate, docs/permissions/permission-sets.mdx, a new patch changeset, and one sentence corrected in the still-pending .changeset/18386-export-import-template.md, which this change made false. Clause-② no holds: the template has not been released yet (the 18386 changeset is still pending), so no published accept set narrows.", "tests": "All at head 501dca7347 (code blob rest-server.ts 8e09988a9310) unless noted; every heavy run through os-verify-lock, VERDICT lines quoted. (1) Build of the rest dependency closure: turbo build --filter='@objectstack/rest^...' --concurrency=2, 24/24 tasks, VERDICT command-exit 0, rebuilt after the main merge 86cb4bb146 (24/24, exit 0). (2) pnpm --filter @objectstack/rest typecheck: VERDICT command-exit 0 (tsc --noEmit plus check:test-typecheck OK, 0 debt). The test file is in tsconfig.test.json's program (tsc --listFilesOnly: 1 hit). (3) Full package suite, pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2: Test Files 248 passed (248), Tests 4962 passed, 114 skipped (5076), VERDICT command-exit 0. Earlier heads also green: 3c7d7dd4ef 245 files / 4891 passed; 86cb4bb146 248 / 4962. (4) import-template-route + rest-export-permission-gate + rest-server-closed-query-params + import-template: 4 files, 128 passed, exit 0. (5) Ablations, each via scripts/ablation-replace.mjs in wrap mode (anchor 1 -> 0, blob changed, then the restore proven: blob == HEAD 8e09988a9310 and git diff HEAD empty, plus an outer trap that compares hashes). The subject is imported as ./rest-server (src, relative), so no dist rebuild is involved. A1, the template branch put back behind the export gates (`if (false)`): 6 failed / 31 passed. Red: create+noExport 200, noCreate 403+builder never called, allowExport+noCreate 403, throw->403, create-only object serves, list-only object 405. Green: preservation. A2, caller create gate deleted: 4 failed / 33. Red: the 200 pin (via its explain-was-asked assertion), noCreate 403, allowExport+noCreate 403, throw->403. A3, every request routed through the import gates (`if (true)`): 2 failed / 35. Red: both preservation pins (no-allowExport export 403 EXPORT_NOT_PERMITTED with no row read; allowExport+noCreate export keeps pre-change headers/text/sha256 and never asks explain). A5, the object half judged by export exposure: 2 failed / 35. Red: both object-half pins. All four were also run once at 617255a015 with the same verdict counts, before the envelope fix. (6) node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: exit 0, 93 commands for the 5 changed paths. All 93 were run at 501dca7347, each exit captured before any pipe. 89 exited 0 on the first pass. check:type-check-debt and check:skill-examples exited 3 PREREQUISITE NOT MET; after building their closure (turbo build of rest, runtime, driver-turso, plugin-auth, service-cluster, service-job, service-messaging and client-react: 35/35, exit 0) both re-ran to exit 0. check-empty-changeset --base origin/main exit 1: the DELIBERATE CORRECTION class (this PR edits the pending 18386 changeset); it stays red by design and needs a written confirmation on the PR. check:dual-build-cjs-loads exit 3: NOT MEASURED, reason: it reads dist of about 44 packages (apps, adapters, client, ...), which only a full build provides; CI builds them. Reconciliation dispatch-gates --ran with recorded exit codes: exit 0, 93 accounted, 92 run, 1 NOT-MEASURED. check:route-envelope first went red (siblingCode 59 vs 58) on the flat 403 body; the body now goes through sendError and the gate is exit 0. (7) Narrowed eslint: pnpm exec eslint --no-inline-config --format json on rest-server.ts + import-template-route.test.ts, exit 0, 2 files in the json, 0 errors, 0 warnings, at 501dca7347. Population from eslint.config.mjs: the files glob '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}' (line 971) covers both, and .md/.mdx sit in no eslint glob. Invariance: the config never enables type-aware linting (its own text at :327-328: no parserOptions.project), and no export changed, so no untouched file's verdict can move. (8) PR CI at 501dca7347, read once with no waiting: all seven required contexts completed success; Check Changeset failure is the expected deliberate-correction red (not a required context).", "mcp_calls": "0", "api_writes": "3 relay writes, each one POST /repos/objectstack-ai/objectstack/dispatches executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, POST /repos/objectstack-ai/objectstack/pulls (draft), run 36793595529, read-back 6360 bytes identical; (2) label-write --assign os-justin, POST /repos/objectstack-ai/objectstack/issues/20977/assignees, run 36793656255, read-back matches; (3) this os-dev-report, POST /repos/objectstack-ai/objectstack/issues/20896/comments via post-stamped.mjs. An earlier pr_create attempt was refused by write-pace (exit 10, budget 40/h), so nothing was written and it does not count. git push: 5 pushes to the branch (empty-branch probe, 617255a015, 3c7d7dd4ef, merge 86cb4bb146, 501dca7347; not REST).", "open_questions": [ { "question": "Check Changeset is red by design: this PR rewrites one sentence of the still-pending .changeset/18386-export-import-template.md. Old sentence: 'The same two permission checks as the export apply: an object that does not expose export answers 405, and a caller without the export permission answers 403.' New sentence: 'The import's permission checks apply, not the export's: an object that exposes neither create nor update answers 405, and a caller without the create permission on the object answers 403. The export permission (allowExport) is not needed.' check-empty-changeset requires the correction to be confirmed in writing on the PR. Confirm it?", "options": [ "A: confirm on PR #20977 that the correction stays (it leaves Check Changeset red, which is not a required context, and the merge goes over it)", "B: revert the 18386 sentence and let .changeset/20896-template-import-door.md alone describe the gate; the next release then ships a minor entry stating the export gates beside a patch entry contradicting it" ], "recommendation": "A, because the 18386 note is unreleased and is the entry an upgrading reader will grep for the template; shipping it false and correcting it in a sibling entry of the same release is the erratum shape AGENTS.md refuses for CHANGELOG text." } ], "out_of_scope_findings": [ "carrier: 承接者:无 · noted, not filed — the create verdict is composed from ISecurityService.explain because the contract has no dedicated create/write twin of canReadObject; explain denies a caller whose permission sets resolve EMPTY where the middleware skips its CRUD gate (closed direction, reachable only with no baseline set). Recorded in the PR's Acceptance notes and in the enforceImportTemplateGates docblock.", "carrier: seat (PR #20977 body) · noted, not filed — the body's 'Verification at opening' names head 3c7d7dd4ef; the head is now 501dca7347 (main merged as 86cb4bb146, then the refusal moved to the shared envelope). Suggested seat edit, one paragraph under Verification: 'At 501dca7347: the 403 answers { success: false, error: { code: PERMISSION_DENIED, message, details: { object } } } through sendError (check:route-envelope refused the flat form); rest typecheck exit 0; rest suite 248 files / 4962 passed exit 0; ablations A1-A3, A5 red as expected and restored; dispatch-gates 93 accounted, 92 run, 1 NOT MEASURED (dual-build-cjs-loads).'" ] }Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Ruling on report
5922602955· 2026-10-01T01:04Zdomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5921450654.501dca7347, the head the report names. It has 5 files (+241 / -32), is a draft, is assigned to os-justin, and saysCloses #20896followed by the claim's Clause-② line.writeModegates are object-level, and create is judged per written row by the engine's middleware. So the template branch reuses stage 1 (enforceApiAccess(..., 'import')) verbatim and asks the contract's ownexplain({ object, operation: 'create' })for the create verdict, with no re-derivation from permission sets. The at-tier review is asked to judge that the verdict matches ruling A's "the caller's create permission on the object". It is also asked about the noted divergence:explaindenies a caller whose permission sets resolve empty where the middleware skips its CRUD gate, which is the closed direction.templateColumnstakes no write mode..changeset/18386-export-import-template.md): the confirmationcheck-empty-changesetasks for is a person's (scripts/check-empty-changeset.mjs:605-612), not the seat's. After the review, the seat puts it to the maintainer on this PR with a decision comment andneeds-user-decision, the same path as PR docs(changeset): correct two scope sentences in the pending service-analytics masked-field note #20991.needs:contract-reviewgoes on the PR and this card, then the at-tier review. objectui#9600's pointer is posted at landing.Generated by Claude Code
revert(changeset): leave the pending 18386 release note as it stands …
objectstack-fleet commentedon Oct 1, 2026
os-dev-report
{ "issue": 20896, "status": "done", "branch": "claude/issue-20896-template-import-door", "pr": "https://github.com/objectstack-ai/objectstack/pull/20977", "session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (subagent run; the parent session's id)", "premise_still_valid": true, "summary": "Patch round after the maintainer chose B on decision 5922804275, per the seat's comment 5923146385 on PR #20977. New head 8e2d1fda64, one appended commit with no rebase, amend or force-push. It restores .changeset/18386-export-import-template.md from the merge base 525b8139a2. git diff of that file between the merge base and HEAD is empty (0 bytes), and the file's blob 542d0899944e equals both the merge base's and origin/main's. Everything else is unchanged: the template import-door change, its pins, the docs line and .changeset/20896-template-import-door.md. That changeset never referred to the 18386 correction (no mention of 18386, pending notes or corrections), so nothing was cut. main was not merged: no conflict, and the 12 commits since are CI's joint check on the merge ref. They touch other files of packages/rest (error-response.ts, tests, tsconfig.json), but not this diff's files. PR body untouched, as instructed.", "tests": "At 8e2d1fda64. node scripts/check-empty-changeset.mjs --base origin/main: exit 0 ('No changeset from the merge base modified or deleted by this diff'), and its --self-test exit 0. check-changeset-no-major --base origin/main: exit 0. check-adr-0087-registration --base origin/main: exit 0. pnpm --filter @objectstack/rest typecheck: VERDICT command-exit 0. Full rest suite (pnpm --filter @objectstack/rest exec vitest run --project local --maxWorkers=2): Test Files 248 passed (248), Tests 4962 passed, 114 skipped (5076), VERDICT command-exit 0. node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack: exit 0, 93 commands for 4 changed paths. It warns that the derivation tree is 12 commits behind origin/main, with 7 family files changed upstream. All 93 were run with exit codes captured before any pipe: 92 exit 0. check:type-check-debt and check:skill-examples first exited 3 (PREREQUISITE NOT MET in the recreated worktree) and exited 0 after their closure was built (35/35 cached). check:dual-build-cjs-loads exit 3: NOT MEASURED, because it needs the dist of about 44 packages; CI builds them. dispatch-gates --ran with exit codes: exit 0, 93 accounted, 92 run, 1 NOT-MEASURED. check:nul-bytes exit 0. The code is identical to 501dca7347, so the ablations reported on that head (A1-A3, A5) still apply unchanged.", "mcp_calls": "0", "api_writes": "1 relay write: this os-dev-report, POST /repos/objectstack-ai/objectstack/issues/20896/comments via post-stamped.mjs (relay dispatch POST /repos/objectstack-ai/objectstack/dispatches). git push: 1 (8e2d1fda64, not REST).", "open_questions": [], "out_of_scope_findings": [] }Generated by Claude Code
objectstack-fleet commentedon Oct 1, 2026
Landed: PR #20977 →
8f784959cf, and the card is donedomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-10-01. Holder of claim5921450654.8f784959cf, with one parent (bb2eccf7ed, read withgit rev-list --parents). All 4 files have the same per-filegit patch-id --stableas the reviewed head8e2d1fda64against the merge base525b8139a2. Review records:5922797800, then the delta PASS5923454406.GET /data/:object/export?template=trueis judged by the import door's ownenforceApiAccess(..., 'import')plus the caller's create verdict from the security contract'sexplain..changeset/18386-export-import-template.mdships as written, beside this PR's patch entry.Closes #20896at merge. The seat now removespm:dispatchedand the assignee. The objectui#9600 pointer is posted in the same act.Generated by Claude Code
fix(rest): the import template answers to the import door's gates, no…