@sema-agent/sdk 6.12.0 → 6.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +96 -0
- package/dist/client.d.ts +9 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +11 -0
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +15 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +27 -2
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +18 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js.map +1 -1
- package/dist/index.d.ts +21 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/resources/adoption.d.ts +196 -0
- package/dist/resources/adoption.d.ts.map +1 -0
- package/dist/resources/adoption.js +55 -0
- package/dist/resources/adoption.js.map +1 -0
- package/dist/resources/rules.d.ts +381 -0
- package/dist/resources/rules.d.ts.map +1 -0
- package/dist/resources/rules.js +120 -0
- package/dist/resources/rules.js.map +1 -0
- package/dist/resources/tool-approvals.d.ts +117 -2
- package/dist/resources/tool-approvals.d.ts.map +1 -1
- package/dist/resources/tool-approvals.js +12 -0
- package/dist/resources/tool-approvals.js.map +1 -1
- package/dist/resources/trace.d.ts +30 -1
- package/dist/resources/trace.d.ts.map +1 -1
- package/dist/resources/trace.js +38 -0
- package/dist/resources/trace.js.map +1 -1
- package/dist/types.d.ts +82 -0
- package/dist/types.d.ts.map +1 -1
- package/openapi.yaml +1266 -3
- package/package.json +1 -1
package/openapi.yaml
CHANGED
|
@@ -135,6 +135,24 @@ tags:
|
|
|
135
135
|
MF-Fleet (shell-host data contract) — the LIVE multi-row fleet view SSE (active task + workflow rows,
|
|
136
136
|
transitioning over time). Owner-scoped by the credential principal (scope wire-stripped) unless fleet-wide;
|
|
137
137
|
in-process bus, NOT resumable (a drop is a full restart). No fleet wiring → 501.
|
|
138
|
+
- name: rules
|
|
139
|
+
description: >
|
|
140
|
+
#154 车二 (design/179 §7) — PERSISTENT permission rules: the CC-settings import lane (prepare → redeem).
|
|
141
|
+
Two-step consent protocol: prepare only previews and mints a one-shot, principal-bound, expiring,
|
|
142
|
+
payload-bound ticket; only redeem reaches the writer. LANE = PRINCIPAL (a rule means "this person
|
|
143
|
+
themself said yes"), never operator. GATED AND OFF BY DEFAULT: the whole lane hangs on
|
|
144
|
+
PERMISSION_RULES_ENABLED (default false) plus a wired rule store — so on a stock deployment BOTH doors
|
|
145
|
+
answer 501 `capability.rule_store_required`, and so does the single-card half (the ask frame simply
|
|
146
|
+
carries no `ruleSuggestions`). Read the observed frame / an actual response, never a version number.
|
|
147
|
+
The SINGLE-CARD half of the same lane is `persistRule` on POST /v1/tool-approvals/{id}/respond.
|
|
148
|
+
- name: adoption
|
|
149
|
+
description: >
|
|
150
|
+
design/183 — identity ADOPTION (form b: pure identity rebind). A one-shot, operator-only DEPLOYMENT
|
|
151
|
+
action; nothing billed. Idempotent in the sense that re-running with the same parameters does not start a
|
|
152
|
+
second adoption and, ONCE TERMINAL, replays the SAME FROZEN `immutableReport` byte-for-byte — the receipt
|
|
153
|
+
as a whole carries no such guarantee (`current` is recomputed every read, and a `stalled` report is a
|
|
154
|
+
provisional projection). Requires a SQL backend (→ else 501). Refusals never produce a receipt — they are
|
|
155
|
+
typed 409s. ⚠️ The GET does not sweep late rows but CAN resume an in-flight arc — it is not a safe poll.
|
|
138
156
|
- name: leader
|
|
139
157
|
description: v2 leader pipeline (gated; E2B + git push). NOT the default door-B brain.
|
|
140
158
|
- name: metrics
|
|
@@ -2530,6 +2548,67 @@ paths:
|
|
|
2530
2548
|
'404': { $ref: '#/components/responses/NotFound' }
|
|
2531
2549
|
'501': { $ref: '#/components/responses/NotImplemented' }
|
|
2532
2550
|
|
|
2551
|
+
/v1/tasks/{taskId}/tool-results/{ref}:
|
|
2552
|
+
parameters:
|
|
2553
|
+
- $ref: '#/components/parameters/PrincipalHeader'
|
|
2554
|
+
- $ref: '#/components/parameters/TaskIdPath'
|
|
2555
|
+
- in: path
|
|
2556
|
+
name: ref
|
|
2557
|
+
required: true
|
|
2558
|
+
schema: { type: string }
|
|
2559
|
+
description: >
|
|
2560
|
+
The opaque tool-result ref the model was handed in the offload preview
|
|
2561
|
+
(`<persisted-output ref="tr_…">`). Pass it VERBATIM (URL-encoded) — never re-derive it.
|
|
2562
|
+
get:
|
|
2563
|
+
tags: [trace]
|
|
2564
|
+
operationId: traceToolResult
|
|
2565
|
+
x-status: live # server: routes/trace-usage.ts `handleToolResult` (blackboard [3321]).
|
|
2566
|
+
summary: Page back an offloaded (large) tool result.
|
|
2567
|
+
description: >
|
|
2568
|
+
The wire mirror of the engine's `ToolResultStore.get`: when a tool returns more text than the
|
|
2569
|
+
offload threshold, the conversation keeps a preview + a `ref` and the full text moves into the
|
|
2570
|
+
deployment's durable tool-result store. This endpoint reads a SLICE of it back, so a workspace/
|
|
2571
|
+
shell can show the full output the model paged through.
|
|
2572
|
+
|
|
2573
|
+
🔴 The 404 is DELIBERATELY uniform (`not_found.tool_result`, one fixed message, the ref never
|
|
2574
|
+
echoed): a ref this task's log does not attest (unknown, another session's, another task's), a
|
|
2575
|
+
deployment with no durable tool-result store, and an attested ref whose content was already
|
|
2576
|
+
reaped are byte-identical responses. Do not branch on them — you cannot tell them apart, by
|
|
2577
|
+
design (a distinguishable arm would be an existence/deployment-shape oracle). In particular this
|
|
2578
|
+
face never 501s on a missing store, and a 404 never proves the output is gone.
|
|
2579
|
+
|
|
2580
|
+
SCOPE — read the ref on the task whose trace showed it. Ownership is the trace family's gate (a
|
|
2581
|
+
principal caller sees only its own runs; a fleet credential is fleet-wide) PLUS an exact binding:
|
|
2582
|
+
the ref must be one this task's own durable event log attests (it is re-derived from the log's
|
|
2583
|
+
tool-call ids). The trace and this face read the same log, so "visible in the task's trace ⇒
|
|
2584
|
+
readable here" holds; a ref carried over from a DIFFERENT task of the same session is a 404 —
|
|
2585
|
+
ask for it under that task's id.
|
|
2586
|
+
parameters:
|
|
2587
|
+
- in: query
|
|
2588
|
+
name: offset
|
|
2589
|
+
required: false
|
|
2590
|
+
schema: { type: integer, minimum: 0, maximum: 2147483646 }
|
|
2591
|
+
description: >
|
|
2592
|
+
Char offset to start the slice at. Absent ⇒ the store's default (0). The cap is the narrowest
|
|
2593
|
+
backend's integer range (the PostgreSQL store binds `offset + 1` as int4) — above it the server
|
|
2594
|
+
answers 400 rather than letting the read fail backend-dependently.
|
|
2595
|
+
- in: query
|
|
2596
|
+
name: limit
|
|
2597
|
+
required: false
|
|
2598
|
+
schema: { type: integer, minimum: 0, maximum: 262144 }
|
|
2599
|
+
description: >
|
|
2600
|
+
Max chars to return. Absent ⇒ the store's default. Above 262144 ⇒ 400 (the server refuses
|
|
2601
|
+
rather than silently clamping, so a caller can never mistake a truncated slice for the ask).
|
|
2602
|
+
responses:
|
|
2603
|
+
'200':
|
|
2604
|
+
description: One slice of the stored content (`totalChars` is the FULL length, not the slice's).
|
|
2605
|
+
content:
|
|
2606
|
+
application/json:
|
|
2607
|
+
schema: { $ref: '#/components/schemas/ToolResultSlice' }
|
|
2608
|
+
'400': { $ref: '#/components/responses/BadRequest' } # request.query_invalid — offset/limit not a non-negative integer, or limit over the cap
|
|
2609
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
2610
|
+
'404': { $ref: '#/components/responses/NotFound' } # not_found.tool_result — the uniform arm (see description)
|
|
2611
|
+
|
|
2533
2612
|
/v1/tasks/{taskId}/stream:
|
|
2534
2613
|
parameters:
|
|
2535
2614
|
- $ref: '#/components/parameters/PrincipalHeader'
|
|
@@ -2806,6 +2885,32 @@ paths:
|
|
|
2806
2885
|
properties:
|
|
2807
2886
|
decision: { type: string, enum: [allow, allow_session, deny] }
|
|
2808
2887
|
updatedInput: { description: 'ctrl+g edited tool args (replaces the asked args on allow).' }
|
|
2888
|
+
persistRule:
|
|
2889
|
+
type: object
|
|
2890
|
+
# 刻意**不**封闭:server 的 `parseToolApprovalResponse` 只读 `rule`,对象里多给的键它**忽略**
|
|
2891
|
+
# (与 `updatedInput` 同一条宽收姿势)。写 `additionalProperties:false` 会让照 spec 生成的
|
|
2892
|
+
# 客户端拒发一个 server 其实会受理的请求 —— 那是我们自己造的漂移,不是上游的约束。
|
|
2893
|
+
required: [rule]
|
|
2894
|
+
properties:
|
|
2895
|
+
rule: { type: string, minLength: 1, maxLength: 512, description: 'Non-empty (the server parser rejects an empty string); at most MAX_RULE_TEXT_CHARS = 512.' }
|
|
2896
|
+
description: >
|
|
2897
|
+
server #154 车二, ADDITIVE — the person ticked "don't ask again": echo back, VERBATIM, the
|
|
2898
|
+
`rule` of the chosen entry of the frame's `ruleSuggestions`. Allow-family only (on `deny`
|
|
2899
|
+
the server tolerantly ignores it, same posture as `updatedInput`).
|
|
2900
|
+
|
|
2901
|
+
🔴 A CANDIDATE, NOT FREE TEXT: the server locates this string in the candidate table the
|
|
2902
|
+
engine minted and refuses if it does not match (`ruleRefusal: "rule_not_offered"`). Do not
|
|
2903
|
+
hand-build it, re-case it or append `:*` — take it from the frame.
|
|
2904
|
+
🔴 Together with `updatedInput` the persistence is refused (`rule_input_edited`): the call
|
|
2905
|
+
that takes effect is the edited one while the candidates came from the ORIGINAL command.
|
|
2906
|
+
The decision itself still stands.
|
|
2907
|
+
⚠️ If the frame carried no `ruleSuggestions`, do not send this key — there is nothing
|
|
2908
|
+
redeemable for THIS card and the answer is necessarily `rulePersisted:false` +
|
|
2909
|
+
`rule_lane_unavailable` (the missing material short-circuits BEFORE any candidate
|
|
2910
|
+
comparison, so it is never `rule_not_offered`). Absence has THREE possible causes — no rule
|
|
2911
|
+
store wired, the engine minted no candidate for this command (compound / redirect /
|
|
2912
|
+
substitution), or the command bytes were unreadable — so do NOT diagnose the deployment's
|
|
2913
|
+
rule lane from one frame or from that refusal.
|
|
2809
2914
|
responses:
|
|
2810
2915
|
'200':
|
|
2811
2916
|
description: Ack (`decision` echoed).
|
|
@@ -3855,6 +3960,376 @@ paths:
|
|
|
3855
3960
|
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
|
3856
3961
|
'501': { $ref: '#/components/responses/NotImplemented' }
|
|
3857
3962
|
|
|
3963
|
+
# ── #154 车二(design/179 §7)—— 持久权限规则的 CC settings 导入两口 ─────────────────────────────
|
|
3964
|
+
# 两步同意协议:prepare 产出预览 + 一张票(**零规则落库**,但 pending 记录与票本身是 durable 的 ⇒ 不幂等),
|
|
3965
|
+
# redeem 才够得着写面。lane = principal(用户导的是
|
|
3966
|
+
# 他自己的规则),NOT operator。整条车道挂 PERMISSION_RULES_ENABLED,未接规则店 ⇒ 两口 501。
|
|
3967
|
+
/v1/rules/cc-import/prepare:
|
|
3968
|
+
parameters:
|
|
3969
|
+
- $ref: '#/components/parameters/PrincipalHeader'
|
|
3970
|
+
post:
|
|
3971
|
+
tags: [rules]
|
|
3972
|
+
operationId: rulesCcImportPrepare
|
|
3973
|
+
x-status: gated # server routes/rules.ts;PERMISSION_RULES_ENABLED + 规则店装配,缺一 ⇒ 501 capability.rule_store_required。SDK rules.ccImportPrepare() 消费。
|
|
3974
|
+
summary: Preview a CC-settings rule import and mint a one-shot redemption ticket.
|
|
3975
|
+
description: >
|
|
3976
|
+
Read the ALLOW buckets of 1–3 user-editable CC settings layers the caller submits, and answer with
|
|
3977
|
+
what the import WOULD do plus a ticket.
|
|
3978
|
+
|
|
3979
|
+
🔴 NO PERMISSION RULE is persisted by this call — only POST /v1/rules/cc-import/redeem reaches the
|
|
3980
|
+
writer. But the call is NOT free of durable state and NOT idempotent: every successful prepare writes
|
|
3981
|
+
a PENDING approval record and mints ANOTHER valid ticket. So re-previewing in a loop accumulates
|
|
3982
|
+
pending records and live tickets; preview once, show it, then redeem or drop it. (A candidate-cap
|
|
3983
|
+
rejection tries to discard the record it just created, best-effort — a leftover orphan pending record
|
|
3984
|
+
affects no adjudication, but it is not a promise of zero residue either.)
|
|
3985
|
+
|
|
3986
|
+
Only the allow bucket is read. The deny/ask buckets are the TIGHTENING direction and have their own
|
|
3987
|
+
channel; importing them through a loosening lane would be the wrong door.
|
|
3988
|
+
|
|
3989
|
+
The ticket is principal-bound, expiring (10 min), single-use (atomically consumed) and payload-bound
|
|
3990
|
+
(it pins a digest of the candidate set at mint time). It is NOT a cacheable credential and must never
|
|
3991
|
+
be handed to another user.
|
|
3992
|
+
|
|
3993
|
+
LANE = PRINCIPAL, not operator: a rule means "this person themself said yes", so an operator has no
|
|
3994
|
+
position on this path — an operator importing their OWN rules walks the same route under their own
|
|
3995
|
+
principal.
|
|
3996
|
+
requestBody:
|
|
3997
|
+
required: true
|
|
3998
|
+
content:
|
|
3999
|
+
application/json:
|
|
4000
|
+
schema:
|
|
4001
|
+
type: object
|
|
4002
|
+
# 封闭:server 侧 zod 是 `.strict()` —— 多一个键是 400,不是静默忽略(把 `content` 拼成
|
|
4003
|
+
# `contents` 的客户端应当场知道,而不是拿到一份「零候选」的预览去纳闷)。
|
|
4004
|
+
additionalProperties: false
|
|
4005
|
+
required: [layers]
|
|
4006
|
+
properties:
|
|
4007
|
+
layers:
|
|
4008
|
+
type: array
|
|
4009
|
+
minItems: 1
|
|
4010
|
+
maxItems: 3
|
|
4011
|
+
items: { $ref: '#/components/schemas/CcImportLayer' }
|
|
4012
|
+
responses:
|
|
4013
|
+
'200':
|
|
4014
|
+
description: >
|
|
4015
|
+
What the import would do, plus the redemption ticket. NO PERMISSION RULE is in the store yet — but a
|
|
4016
|
+
pending approval record and this ticket are already durable (this call is not idempotent; see the
|
|
4017
|
+
operation description).
|
|
4018
|
+
content:
|
|
4019
|
+
application/json:
|
|
4020
|
+
schema: { $ref: '#/components/schemas/CcImportPrepareResult' }
|
|
4021
|
+
'400': { $ref: '#/components/responses/BadRequest' } # request.body_shape — not {layers:[{layer,path,root,content}]}, or an empty `root` (an empty prefix would silently promote a project rule to global)
|
|
4022
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
4023
|
+
'413': { $ref: '#/components/responses/PayloadTooLarge' } # request.payload_too_large — more than 200 candidates across all submitted layers; SHORTEN and resend (retrying the same payload cannot help)
|
|
4024
|
+
'429': { $ref: '#/components/responses/RateLimited' }
|
|
4025
|
+
'501': { $ref: '#/components/responses/NotImplemented' } # capability.rule_store_required — this deployment has no permission-rule store wired
|
|
4026
|
+
|
|
4027
|
+
/v1/rules/cc-import/redeem:
|
|
4028
|
+
parameters:
|
|
4029
|
+
- $ref: '#/components/parameters/PrincipalHeader'
|
|
4030
|
+
post:
|
|
4031
|
+
tags: [rules]
|
|
4032
|
+
operationId: rulesCcImportRedeem
|
|
4033
|
+
x-status: gated # server routes/rules.ts;同 prepare 的部署谓词。SDK rules.ccImportRedeem() 消费。
|
|
4034
|
+
summary: Redeem a prepared import ticket — the ONE step that reaches the rule writer.
|
|
4035
|
+
description: >
|
|
4036
|
+
Atomically consume the ticket, confirm the pending approval record, and apply the batch. Idempotent
|
|
4037
|
+
at the engine level (each candidate carries its own dot), but the TICKET is single-use — do not
|
|
4038
|
+
auto-retry this call, and never replay it speculatively.
|
|
4039
|
+
|
|
4040
|
+
🔴 THE 404 IS UNIFORM BY DESIGN (`not_found.rule_ticket`, one fixed message): a forged ticket,
|
|
4041
|
+
ANOTHER principal's ticket, an expired one and an already-consumed one are byte-identical responses.
|
|
4042
|
+
Distinguishable arms would be an existence oracle (probing whether someone else's ticket exists).
|
|
4043
|
+
⚠️ It is also WIDER than those four: non-retryable record/payload failures (the approval record is gone,
|
|
4044
|
+
the payload digest no longer matches, an adjudicated confirmation refusal) fold into the SAME 404. So do
|
|
4045
|
+
NOT branch on the cause and do NOT read it as "the user brought the wrong ticket" — treat any 404 as
|
|
4046
|
+
"this ticket is not usable here, start over".
|
|
4047
|
+
|
|
4048
|
+
The ONE distinguishable arm is 503 `state.rule_import_retry`: it means the claim was RELEASED and
|
|
4049
|
+
the ticket is STILL USABLE — retry as-is after `retryAfterSec` (echoed in `Retry-After`). Folding
|
|
4050
|
+
that into the 404 would tell a caller their confirmation is lost when it is not; the two call for
|
|
4051
|
+
opposite handling.
|
|
4052
|
+
requestBody:
|
|
4053
|
+
required: true
|
|
4054
|
+
content:
|
|
4055
|
+
application/json:
|
|
4056
|
+
schema:
|
|
4057
|
+
type: object
|
|
4058
|
+
additionalProperties: false # server 侧 `.strict()`
|
|
4059
|
+
required: [ticket]
|
|
4060
|
+
properties:
|
|
4061
|
+
ticket: { type: string, minLength: 1, maxLength: 190, description: 'The ticket from rulesCcImportPrepare. Verbatim.' }
|
|
4062
|
+
responses:
|
|
4063
|
+
'200':
|
|
4064
|
+
description: What the import ACTUALLY did (a different moment and contract from the preview).
|
|
4065
|
+
content:
|
|
4066
|
+
application/json:
|
|
4067
|
+
schema: { $ref: '#/components/schemas/CcImportRedeemResult' }
|
|
4068
|
+
'400': { $ref: '#/components/responses/BadRequest' } # request.body_shape — body is not {ticket}
|
|
4069
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
4070
|
+
'404': { $ref: '#/components/responses/NotFound' } # not_found.rule_ticket — the UNIFORM arm (forged / another principal's / expired / already used). Never branch on the cause.
|
|
4071
|
+
'429': { $ref: '#/components/responses/RateLimited' }
|
|
4072
|
+
'503':
|
|
4073
|
+
description: >
|
|
4074
|
+
errorCode `state.rule_import_retry` — the import could not complete right now (a transient store
|
|
4075
|
+
wobble), the claim was released and THE TICKET IS STILL USABLE. Body carries `retryAfterSec`
|
|
4076
|
+
and the same value rides `Retry-After`. Retry the SAME ticket. This is the only arm on this
|
|
4077
|
+
endpoint a caller may distinguish from the uniform 404, and the distinction is load-bearing.
|
|
4078
|
+
headers:
|
|
4079
|
+
Retry-After:
|
|
4080
|
+
schema: { type: integer }
|
|
4081
|
+
description: Seconds to wait before retrying with the same ticket.
|
|
4082
|
+
content:
|
|
4083
|
+
application/json:
|
|
4084
|
+
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
|
4085
|
+
'501': { $ref: '#/components/responses/NotImplemented' } # capability.rule_store_required
|
|
4086
|
+
|
|
4087
|
+
# ── design/203 §2(server 7.12.0)—— 持久权限规则的**撤销面**两口。与上面的导入两口共用部署谓词
|
|
4088
|
+
# (PERMISSION_RULES_ENABLED + 规则店装配,缺一 ⇒ 501),但多一条 operator 越权域:`principal` 让一个
|
|
4089
|
+
# 列了名的 operator 读或收回**任一租户**的规则。方向决定授权 —— 收回是 TIGHTENING(core removePersistedRule
|
|
4090
|
+
# 头注逐字:a host may narrow on a user's behalf, it may not widen),而回收权限正是撤销面的本义。
|
|
4091
|
+
# 🔴 路径**没有 :id 段**:一条规则的身份就是它的 (rule, scope) 内容对,服务端不铸行 id,所以删是按内容删。
|
|
4092
|
+
/v1/rules:
|
|
4093
|
+
parameters:
|
|
4094
|
+
- $ref: '#/components/parameters/PrincipalHeader'
|
|
4095
|
+
get:
|
|
4096
|
+
tags: [rules]
|
|
4097
|
+
operationId: rulesList
|
|
4098
|
+
x-status: gated # server routes/rules.ts handleRuleList;同 cc-import 的部署谓词。SDK rules.list() 消费。
|
|
4099
|
+
summary: List the rules that are LIVE under a principal (keyset-paged, cursor bound to the revision).
|
|
4100
|
+
description: >
|
|
4101
|
+
List the persisted allow rules currently live under one principal (tombstones already folded).
|
|
4102
|
+
Ordering is deterministic — `(scope, rule)` lexicographic — because keyset paging's whole premise is
|
|
4103
|
+
that the same data comes back in the same order every time. The order is decided by the server, NOT
|
|
4104
|
+
by the store (core's `list()` promises none).
|
|
4105
|
+
|
|
4106
|
+
🔴 THE CURSOR IS BOUND TO `(rev, principal, scope)` AND A MISMATCH IS REFUSED, NOT RESET. A rule set
|
|
4107
|
+
is read WHOLE, so there is no tearing WITHIN a page — tearing happens BETWEEN pages, when somebody
|
|
4108
|
+
adds or removes a rule after page 1 and the keyset start now lands in a different set (silently
|
|
4109
|
+
dropped or duplicated rows). Sending a cursor minted at another revision / principal / scope filter
|
|
4110
|
+
gets a 400 (`request.query_invalid`, body carries the current `rev`); restart the listing without a
|
|
4111
|
+
cursor. Silently starting over would let a client read page 2 as "continuing page 1" and hand a
|
|
4112
|
+
governance surface a list that is both short and duplicated, with nobody the wiser.
|
|
4113
|
+
|
|
4114
|
+
`scope` FILTER: given but unreadable ⇒ 400, never "treated as unfiltered" — that would silently turn
|
|
4115
|
+
"show me only this project" into a full listing the caller would accept as the project's list.
|
|
4116
|
+
`limit` is CLAMPED (not refused) into 1..200, default 50 — a mistyped paging knob must not make a
|
|
4117
|
+
governance page unopenable.
|
|
4118
|
+
|
|
4119
|
+
OPERATOR OVERRIDE: `principal` reads ANOTHER tenant's rules and is `OPERATOR_PRINCIPALS`-gated
|
|
4120
|
+
(an EMPTY list means NOBODY — never inferred from the service token, which is a shared deployment
|
|
4121
|
+
credential, not a person). Omitted, or equal to your own principal, is the ordinary self-read and
|
|
4122
|
+
needs no operator standing.
|
|
4123
|
+
parameters:
|
|
4124
|
+
- name: principal
|
|
4125
|
+
in: query
|
|
4126
|
+
required: false
|
|
4127
|
+
schema: { type: string, minLength: 1, maxLength: 190 }
|
|
4128
|
+
description: >
|
|
4129
|
+
OPERATOR-ONLY cross-tenant read. Absent (or equal to the caller's own principal) = read your own
|
|
4130
|
+
rules. Anything else requires explicit OPERATOR_PRINCIPALS membership ⇒ otherwise 403
|
|
4131
|
+
`auth.operator_only`. Rule text contains command shapes, so third-party reads are an information
|
|
4132
|
+
disclosure and are deliberately not opened to non-operators.
|
|
4133
|
+
- name: scope
|
|
4134
|
+
in: query
|
|
4135
|
+
required: false
|
|
4136
|
+
schema: { type: string, minLength: 1 }
|
|
4137
|
+
description: >
|
|
4138
|
+
Filter to one scope, spelled `global` or `project:<root>` with a NON-EMPTY root. Unreadable ⇒ 400
|
|
4139
|
+
`request.query_invalid` (never silently unfiltered). Matching is done on the CANONICAL form, so an
|
|
4140
|
+
equivalent-but-differently-spelled root filters to an empty page.
|
|
4141
|
+
- name: limit
|
|
4142
|
+
in: query
|
|
4143
|
+
required: false
|
|
4144
|
+
schema: { type: integer, minimum: 1, maximum: 200, default: 50 }
|
|
4145
|
+
description: 'Page size. CLAMPED into 1..200 (non-numeric / out-of-range is clamped, not refused); default 50.'
|
|
4146
|
+
- name: cursor
|
|
4147
|
+
in: query
|
|
4148
|
+
required: false
|
|
4149
|
+
schema: { type: string }
|
|
4150
|
+
description: >
|
|
4151
|
+
The `nextCursor` from the previous page, VERBATIM (base64url, opaque — do not parse or mint one).
|
|
4152
|
+
It is bound to the revision, the principal and the scope filter it was minted on; any mismatch is
|
|
4153
|
+
a 400 `request.query_invalid` carrying the current `rev`. Restart the listing without a cursor.
|
|
4154
|
+
responses:
|
|
4155
|
+
'200':
|
|
4156
|
+
description: One page of live rules plus the store revision the page was read at.
|
|
4157
|
+
content:
|
|
4158
|
+
application/json:
|
|
4159
|
+
schema: { $ref: '#/components/schemas/RuleListResult' }
|
|
4160
|
+
'400': { $ref: '#/components/responses/BadRequest' } # request.query_invalid — unreadable `scope`, unreadable `cursor`, or a cursor minted at a different (rev, principal, scope). Body carries `rev` on the revision-mismatch arm.
|
|
4161
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
4162
|
+
'403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only — reading another principal's rules without explicit OPERATOR_PRINCIPALS membership (an EMPTY list means nobody)
|
|
4163
|
+
'429': { $ref: '#/components/responses/RateLimited' }
|
|
4164
|
+
'501': { $ref: '#/components/responses/NotImplemented' } # capability.rule_store_required — this deployment has no permission-rule store wired
|
|
4165
|
+
delete:
|
|
4166
|
+
tags: [rules]
|
|
4167
|
+
operationId: rulesRevoke
|
|
4168
|
+
x-status: gated # server routes/rules.ts handleRuleRevoke;同 cc-import 的部署谓词。SDK rules.revoke() 消费。
|
|
4169
|
+
summary: Revoke a persisted rule BY CONTENT — `(rule, scope)`, with no id in the path.
|
|
4170
|
+
description: >
|
|
4171
|
+
Revoke one persisted allow rule identified by its CONTENT pair `(rule, scope)`. There is no `:id`
|
|
4172
|
+
path segment on purpose: a rule's identity IS that pair, the server mints no row id, and the engine's
|
|
4173
|
+
`removePersistedRule` primitive (which this endpoint always goes through) works in observed-remove
|
|
4174
|
+
form over the add dots.
|
|
4175
|
+
|
|
4176
|
+
🔴 IDEMPOTENT, AND DELIBERATELY NOT A 404. Revoking twice, or revoking something that was never
|
|
4177
|
+
there, is `200 {status:"no-op"}` — a "did this rule ever exist?" 404 would be an existence oracle
|
|
4178
|
+
(a CROSS-TENANT one under the operator override), and deletion has no business answering that
|
|
4179
|
+
question. Read `status` to learn whether you removed anything.
|
|
4180
|
+
|
|
4181
|
+
🔴 `stillLive` IS LOAD-BEARING ON BOTH ARMS. `true` means the tombstone landed but the rule is
|
|
4182
|
+
STILL live under add-wins — a fresh approval was recorded during the call. Folding that into a plain
|
|
4183
|
+
200 would let a client read an INCOMPLETE revocation as complete, which is precisely what a
|
|
4184
|
+
governance surface must never get wrong. The `no-op` arm carries it for the same reason: the engine
|
|
4185
|
+
answers `no-op` off its initial snapshot without a read-back, so an approval landing in that window
|
|
4186
|
+
leaves the rule alive while the verb says "nothing to remove". Both arms therefore ship the same key
|
|
4187
|
+
set and one consumer branch reads both.
|
|
4188
|
+
|
|
4189
|
+
OPERATOR OVERRIDE: `principal` revokes on ANOTHER tenant's behalf, gated exactly like the read half
|
|
4190
|
+
(explicit `OPERATOR_PRINCIPALS`; empty list = nobody). The direction is what makes the override
|
|
4191
|
+
legitimate — revoking is TIGHTENING — but the identity test does not relax because of it.
|
|
4192
|
+
requestBody:
|
|
4193
|
+
required: true
|
|
4194
|
+
content:
|
|
4195
|
+
application/json:
|
|
4196
|
+
schema: { $ref: '#/components/schemas/RuleRevokeRequest' }
|
|
4197
|
+
responses:
|
|
4198
|
+
'200':
|
|
4199
|
+
description: >
|
|
4200
|
+
The revocation outcome. `removed` = a tombstone was written; `no-op` = nothing matched (idempotent
|
|
4201
|
+
repeat, or it was never there). BOTH arms carry `stillLive` — read it before telling a human the
|
|
4202
|
+
rule is gone.
|
|
4203
|
+
content:
|
|
4204
|
+
application/json:
|
|
4205
|
+
schema: { $ref: '#/components/schemas/RuleRevokeResult' }
|
|
4206
|
+
'400': { $ref: '#/components/responses/BadRequest' } # request.body_shape — body is not {rule, scope, principal?}, or `scope` is not `global`/`project:<non-empty root>`
|
|
4207
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
4208
|
+
'403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only — revoking another principal's rule without explicit OPERATOR_PRINCIPALS membership
|
|
4209
|
+
'429': { $ref: '#/components/responses/RateLimited' }
|
|
4210
|
+
'503':
|
|
4211
|
+
description: >
|
|
4212
|
+
errorCode `state.rule_remove_failed` — the engine could not complete the removal right now. Core's
|
|
4213
|
+
contract for this arm is "a read-back confirmed nothing was written, OR the read-back itself failed
|
|
4214
|
+
and the outcome is undetermined" — both are RETRY-CAN-CHANGE-IT, which is why it is a 503 and not a
|
|
4215
|
+
500. Retry, then reconcile with `GET /v1/rules`.
|
|
4216
|
+
content:
|
|
4217
|
+
application/json:
|
|
4218
|
+
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
|
4219
|
+
'501': { $ref: '#/components/responses/NotImplemented' } # capability.rule_store_required
|
|
4220
|
+
|
|
4221
|
+
# ── design/183 —— 身份收编面(form b:纯身份重绑)。operator-only 的一次性部署级动作,零模型工作。 ──
|
|
4222
|
+
/v1/adoption:
|
|
4223
|
+
parameters:
|
|
4224
|
+
- $ref: '#/components/parameters/PrincipalHeader'
|
|
4225
|
+
post:
|
|
4226
|
+
tags: [adoption]
|
|
4227
|
+
operationId: adoptionStart
|
|
4228
|
+
x-status: gated # server routes/adoption.ts;需 SQL 后端(DB_BACKEND=mysql|pg),否则 501 capability.adoption_store_required。SDK adoption.start() 消费。
|
|
4229
|
+
summary: Adopt one principal's data under another principal (identity rebind, form b).
|
|
4230
|
+
description: >
|
|
4231
|
+
A one-shot DEPLOYMENT action, not a business surface: operator-only, nothing billed, and honestly 501
|
|
4232
|
+
on a deployment with no SQL backend (the identity axis being rebound does not exist on the local one).
|
|
4233
|
+
|
|
4234
|
+
IDEMPOTENT: re-running with the same parameters never starts a second adoption, and ONCE THE ARC IS
|
|
4235
|
+
TERMINAL it replays the same frozen `immutableReport` byte-for-byte (written once at the final phase,
|
|
4236
|
+
never recomputed). The RECEIPT AS A WHOLE carries no byte-identity guarantee — `current` is recomputed
|
|
4237
|
+
on every call, so two reads may match or may differ — and before the terminal state
|
|
4238
|
+
(`status: "stalled"`) the report itself is a provisional projection of the current row. Concurrent identical POSTs are
|
|
4239
|
+
arbitrated by a UNIQUE on the source principal: exactly one row lands and both connections read the
|
|
4240
|
+
same `adoptionId`.
|
|
4241
|
+
|
|
4242
|
+
🔴 A REFUSED ADOPTION NEVER PRODUCES A RECEIPT. `status` is a two-word closed set
|
|
4243
|
+
(`adopted`/`stalled`) with NO `rejected` member — refusals are typed 409s. Otherwise "refused" and
|
|
4244
|
+
"succeeded" would share one 200 envelope and a consumer could only guess by reading fields.
|
|
4245
|
+
|
|
4246
|
+
🔴 This does NOT fence off ordinary writers. "Stop the engine first" is an OPERATOR PRECONDITION, not
|
|
4247
|
+
a machine guarantee: a caller still injecting the old identity keeps writing rows after the terminal
|
|
4248
|
+
state, while the idempotent short-circuit keeps answering `adopted`. That is what
|
|
4249
|
+
`current.residualSourceRows` makes visible — `> 0` means rows MAY still sit under the old identity OR
|
|
4250
|
+
the count itself failed (the server reports 1 fail-closed when it cannot count); either way, go look.
|
|
4251
|
+
It is not proof of an active writer. A POST (a write verb) also sweeps those late rows into the new
|
|
4252
|
+
identity and re-reports the count; a GET does not sweep — but it DOES resume an in-flight arc (see the
|
|
4253
|
+
GET description), so it is not a safe poll either.
|
|
4254
|
+
requestBody:
|
|
4255
|
+
required: true
|
|
4256
|
+
content:
|
|
4257
|
+
application/json:
|
|
4258
|
+
schema: { $ref: '#/components/schemas/AdoptionRequest' }
|
|
4259
|
+
responses:
|
|
4260
|
+
'200':
|
|
4261
|
+
description: >
|
|
4262
|
+
The adoption receipt — the SAME ENVELOPE SHAPE for the first success and for every idempotent
|
|
4263
|
+
re-run. There is NO byte-identity guarantee for the whole receipt (it may match or differ):
|
|
4264
|
+
`current` is recomputed on every call. The one frozen part is a TERMINAL `immutableReport`.
|
|
4265
|
+
content:
|
|
4266
|
+
application/json:
|
|
4267
|
+
schema: { $ref: '#/components/schemas/AdoptionReceipt' }
|
|
4268
|
+
'400': { $ref: '#/components/responses/BadRequest' } # request.body_shape / request.field_invalid — same from/to ("nothing to adopt", refused BEFORE a row is minted so the UNIQUE slot is not burned), or a principal that does not fit every identity column and derived key this adoption must write
|
|
4269
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
4270
|
+
'403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only
|
|
4271
|
+
'409':
|
|
4272
|
+
description: >
|
|
4273
|
+
A refusal — THREE distinct errorCodes ride this status and the operator's next action differs for
|
|
4274
|
+
each, so branch on `errorCode`, never on the status:
|
|
4275
|
+
`adoption.source_already_bound` (this source is already bound to a DIFFERENT destination — a
|
|
4276
|
+
second adoption / cross-tenant transfer, deliberately out of scope; body carries `adoptionId`,
|
|
4277
|
+
`fromPrincipal`, `boundTo`);
|
|
4278
|
+
`adoption.destination_conflict` (the destination already holds rows sharing a logical key with
|
|
4279
|
+
the source; body lists the conflicting tables in `conflicts` — go clean those up);
|
|
4280
|
+
`adoption.destination_unrepresentable` (the destination identity does not fit some key this
|
|
4281
|
+
adoption would DERIVE from it, e.g. a memory `proj:` key overrunning a column width — pick a
|
|
4282
|
+
SHORTER destination principal).
|
|
4283
|
+
Either way both sides are byte-unchanged.
|
|
4284
|
+
content:
|
|
4285
|
+
application/json:
|
|
4286
|
+
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
|
4287
|
+
'501': { $ref: '#/components/responses/NotImplemented' } # capability.adoption_store_required — adoption requires a SQL store backend (DB_BACKEND=mysql|pg)
|
|
4288
|
+
|
|
4289
|
+
/v1/adoption/{adoptionId}:
|
|
4290
|
+
parameters:
|
|
4291
|
+
- $ref: '#/components/parameters/PrincipalHeader'
|
|
4292
|
+
- { name: adoptionId, in: path, required: true, schema: { type: string }, description: 'From AdoptionReceipt.adoptionId.' }
|
|
4293
|
+
get:
|
|
4294
|
+
tags: [adoption]
|
|
4295
|
+
operationId: adoptionGet
|
|
4296
|
+
x-status: gated # server routes/adoption.ts;同 POST 的部署谓词。SDK adoption.get() 消费。
|
|
4297
|
+
summary: Read one adoption's state (and RESUME it if it is still in flight).
|
|
4298
|
+
description: >
|
|
4299
|
+
🔴 THIS IS NOT A SIDE-EFFECT-FREE READ. On a TERMINAL row (adopted / refused) it is a pure read. On a
|
|
4300
|
+
row that is STILL IN FLIGHT — the typical case being a deployment where nothing has POSTed again since
|
|
4301
|
+
a crash — it RESUMES THE ARC: the same `driveUnderLock` the boot scan uses, advancing phases, rewriting
|
|
4302
|
+
rows on the identity axis, recording the config ledger and landing the terminal state. That is
|
|
4303
|
+
deliberate upstream (otherwise one crash would require a human to re-POST), but a consumer must know
|
|
4304
|
+
it: do NOT treat this as a safe prefetch / dashboard poll — reading an in-flight arc is pressing
|
|
4305
|
+
continue on it. The ONLY difference from the POST is that the GET does NOT sweep late rows into the
|
|
4306
|
+
new identity (the POST does). Repeating the call is safe (advisory lock + phase CAS + idempotent
|
|
4307
|
+
identity ⇒ a replay lands where one call lands) — but "safely replayable" is not "read-only". There
|
|
4308
|
+
is no verb on this wire today that guarantees zero progression.
|
|
4309
|
+
|
|
4310
|
+
`immutableReport` is historical fact — but only ONCE `status` is `adopted`; on a `stalled` receipt it is
|
|
4311
|
+
a provisional projection of the current row and can differ between two reads (see the AdoptionReport
|
|
4312
|
+
schema). `current` is recomputed every time. Do not infer one half from the other — in particular
|
|
4313
|
+
`status: "adopted"` does NOT mean the configuration migration is done; that account lives in
|
|
4314
|
+
`current.outstandingConfigs` / `current.residualSourceRows`, and a non-zero residual may also mean the
|
|
4315
|
+
count itself failed (see that field).
|
|
4316
|
+
responses:
|
|
4317
|
+
'200':
|
|
4318
|
+
description: The adoption receipt.
|
|
4319
|
+
content:
|
|
4320
|
+
application/json:
|
|
4321
|
+
schema: { $ref: '#/components/schemas/AdoptionReceipt' }
|
|
4322
|
+
'400': { $ref: '#/components/responses/BadRequest' } # request.id_invalid — undecodable id (shared with sessions/attachments; no second synonym code was minted)
|
|
4323
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
4324
|
+
'403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only
|
|
4325
|
+
'404': { $ref: '#/components/responses/NotFound' } # not_found.adoption — no such id. ⚠️ A REFUSED adoption is still a typed 409 here, not a 404: the refusal is remembered.
|
|
4326
|
+
'409':
|
|
4327
|
+
description: 'Same three refusal codes as adoptionStart (a refused adoption keeps answering its refusal).'
|
|
4328
|
+
content:
|
|
4329
|
+
application/json:
|
|
4330
|
+
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
|
4331
|
+
'501': { $ref: '#/components/responses/NotImplemented' } # capability.adoption_store_required
|
|
4332
|
+
|
|
3858
4333
|
/metrics/summary:
|
|
3859
4334
|
get:
|
|
3860
4335
|
tags: [metrics]
|
|
@@ -4479,6 +4954,35 @@ components:
|
|
|
4479
4954
|
items: { $ref: '#/components/schemas/TaskAgentDefinition' }
|
|
4480
4955
|
description: Per-task sub-agent definitions (roster additions scoped to this task only).
|
|
4481
4956
|
interactiveTools: { type: boolean, description: Mount interactive (prompting) tools. }
|
|
4957
|
+
oneShot:
|
|
4958
|
+
type: boolean
|
|
4959
|
+
description: >
|
|
4960
|
+
core >=5.23.0 `TaskSpec.oneShot`, server >=7.12.0 — declare that THIS SUBMISSION is one-shot: no
|
|
4961
|
+
later turn exists in which an asynchronous background notification could land. The archetype is a
|
|
4962
|
+
headless `sema -p` whose process exits when the turn ends.
|
|
4963
|
+
|
|
4964
|
+
🔴 IT GRANTS NOTHING AND GUARANTEES NOTHING — core consumes it as GUIDANCE ONLY. What changes is
|
|
4965
|
+
the wording the engine gives the MODEL for `RunWorkflow` / delegation receipts: "end your turn, you
|
|
4966
|
+
will be notified" (correct only when a next turn exists) becomes "block-wait via
|
|
4967
|
+
`TaskOutput({block:true})`". Omitting it on a headless call is a known way to LOSE background
|
|
4968
|
+
results — that is why the key exists — but sending it is **best-effort**, not a completion
|
|
4969
|
+
contract: the model may ignore the instruction or run out of budget, and some legs have no poll
|
|
4970
|
+
path at all. If your caller REQUIRES the results, keep your own wait/reconcile step; do not treat
|
|
4971
|
+
`oneShot: true` as one. There is no tenancy gate because there is no capability to gate.
|
|
4972
|
+
|
|
4973
|
+
🔴 PROBE `capabilities.oneShot` BEFORE SENDING IT: the task body is an OPEN set (the server does
|
|
4974
|
+
not refuse unknown keys), so a worker that does not yet consume this field accepts `oneShot: true`,
|
|
4975
|
+
answers 200, and still tells the model to end its turn and await a notification the process will
|
|
4976
|
+
never receive — no 400, no warning. The capability bit (server >=7.12.0) is what distinguishes the
|
|
4977
|
+
two; ABSENT means the key is being ignored. Note the bit being PRESENT does not retire the
|
|
4978
|
+
caller-side wait step: it proves the key is consumed, not that the results arrive.
|
|
4979
|
+
|
|
4980
|
+
PER-REQUEST on purpose: "does this submission expect to be continued" is a property of the
|
|
4981
|
+
SUBMISSION, not of the connection it arrived on. Sibling of `interactiveTools` (the same `-p`
|
|
4982
|
+
posture) and passed through identically — a boolean rides, ABSENT ⇒ the key is omitted and core's
|
|
4983
|
+
default (interactive) applies. Anything that is neither absent nor a boolean is a fail-loud 400
|
|
4984
|
+
`request.field_invalid` ("oneShot must be a boolean"), NOT a silent coercion. Rides the persisted
|
|
4985
|
+
body onto resume legs.
|
|
4482
4986
|
retainBackgroundProcesses: { type: boolean, description: Keep background processes alive past the turn. }
|
|
4483
4987
|
forwardSubagentEvents:
|
|
4484
4988
|
type: boolean
|
|
@@ -4927,8 +5431,12 @@ components:
|
|
|
4927
5431
|
type: object
|
|
4928
5432
|
description: >
|
|
4929
5433
|
Trace turn (true shape, service-pinned). role currently only "assistant" (user/system/tool reserved —
|
|
4930
|
-
the user/objective opening turn is NOT projected today). blocks is an OPEN set
|
|
4931
|
-
|
|
5434
|
+
the user/objective opening turn is NOT projected today). blocks is an OPEN set. tool-result.output IS
|
|
5435
|
+
present (server trace/project.ts projects it; the old "ALWAYS absent" note was stale — [3315] finding) —
|
|
5436
|
+
but note it carries the STORED form: outputs over the engine's offload threshold are persisted in
|
|
5437
|
+
bounded form (head/tail preview + `<persisted-output ref>` marker), so `output` here can be the
|
|
5438
|
+
truncated form, disclosed via `truncated`/`totalChars`. Full retrieval of an offloaded output awaits
|
|
5439
|
+
the tool-results read face ([3316] server half, planned).
|
|
4932
5440
|
required: [seq, ts, role, blocks]
|
|
4933
5441
|
additionalProperties: true
|
|
4934
5442
|
properties:
|
|
@@ -4949,6 +5457,20 @@ components:
|
|
|
4949
5457
|
input: { description: tool-call — redacted args. }
|
|
4950
5458
|
callId: { type: string }
|
|
4951
5459
|
isError: { type: boolean }
|
|
5460
|
+
# 🔴 core >=5.18.1 (#187) —— 与 `Event_tool_end.settledBy` **同一个值**:server 的共享挑键器
|
|
5461
|
+
# `toolResultFieldsOf`(src/trace/project.ts)同时喂 turns 面与 trace SSE 面,而写侧
|
|
5462
|
+
# `toolEndEventData` 喂 durable 账本 —— 三面一个真源,所以这里的词表与那边逐字相同。
|
|
5463
|
+
settledBy:
|
|
5464
|
+
type: string
|
|
5465
|
+
enum: [human, timeout, aborted]
|
|
5466
|
+
description: >
|
|
5467
|
+
tool-result — HOW this call was settled when it went through an approval: `human` = somebody
|
|
5468
|
+
actually answered; `timeout` = the approval window elapsed; `aborted` = abort / unclonable
|
|
5469
|
+
arguments / out-of-contract. Present ONLY on the settled call.
|
|
5470
|
+
ABSENCE CARRIES NO SEMANTICS (an older worker, or a posture arm that deliberately leaves it
|
|
5471
|
+
unset — the two are indistinguishable), so absent is neither "human" nor "no approval
|
|
5472
|
+
happened". The read leg re-validates the closed set independently of the write leg, because a
|
|
5473
|
+
ledger row can come from any engine generation.
|
|
4952
5474
|
tokens:
|
|
4953
5475
|
type: object
|
|
4954
5476
|
properties:
|
|
@@ -4968,6 +5490,9 @@ components:
|
|
|
4968
5490
|
# 于是「server 少发 retainedFrom」这种断页判据丢失对本门不可见。
|
|
4969
5491
|
additionalProperties: false
|
|
4970
5492
|
required: [turns, retainedFrom]
|
|
5493
|
+
# ⚠️ 路别差异契约([3315]② 定谳,core [3316] 判设计事实非缺陷):trace 记账只在 /v1/runs 路;
|
|
5494
|
+
# sync `POST /v1/tasks` 完成后的 turns 读面对该 run 恒 `turns: []`(retainedFrom: 0)。这是契约,
|
|
5495
|
+
# 不是待修缺口——sync 路要 trace 请改走 /v1/runs。哪天 sync 路开始填 turns = 行为变更需过本表。
|
|
4971
5496
|
properties:
|
|
4972
5497
|
turns:
|
|
4973
5498
|
type: array
|
|
@@ -5048,6 +5573,21 @@ components:
|
|
|
5048
5573
|
type: array
|
|
5049
5574
|
items: { $ref: '#/components/schemas/Artifact' }
|
|
5050
5575
|
|
|
5576
|
+
ToolResultSlice:
|
|
5577
|
+
type: object
|
|
5578
|
+
description: >
|
|
5579
|
+
One slice of an offloaded tool result (`GET /v1/tasks/{taskId}/tool-results/{ref}`) — the wire
|
|
5580
|
+
form of the engine's `ToolResultSlice`. `offset` echoes where this slice starts (so successive
|
|
5581
|
+
reads concatenate deterministically) and `totalChars` is the length of the FULL stored content,
|
|
5582
|
+
independent of the slice — a client pages until `offset + content.length === totalChars`.
|
|
5583
|
+
# 封闭:铸造点 routes/trace-usage.ts `handleToolResult` 只发这三键(core ToolResultSlice 原形)。
|
|
5584
|
+
additionalProperties: false
|
|
5585
|
+
required: [content, offset, totalChars]
|
|
5586
|
+
properties:
|
|
5587
|
+
content: { type: string, description: 'The slice text.' }
|
|
5588
|
+
offset: { type: integer, description: 'Char offset this slice starts at.' }
|
|
5589
|
+
totalChars: { type: integer, description: 'Total chars of the FULL stored content (not this slice).' }
|
|
5590
|
+
|
|
5051
5591
|
LeaderReceipt:
|
|
5052
5592
|
type: object
|
|
5053
5593
|
description: >
|
|
@@ -5186,6 +5726,76 @@ components:
|
|
|
5186
5726
|
legacy verbs in 1.0.0; the live face is `client.memory.exportScope` / `.sync`
|
|
5187
5727
|
(`GET /v1/memory/export`, `POST /v1/memory/sync/{scope}`), which this flag does NOT gate.
|
|
5188
5728
|
Kept on the wire so an older shell that still reads it honestly hides the dead affordances.
|
|
5729
|
+
adoption:
|
|
5730
|
+
type: boolean
|
|
5731
|
+
description: >
|
|
5732
|
+
design/183 form b adoption face (POST /v1/adoption + GET /v1/adoption/:id, operator lane) is
|
|
5733
|
+
wired. Same predicate as the routes' 501 (the SQL backend's adoptionLog factory is present;
|
|
5734
|
+
the local backend deliberately omits it). Probe before use, no trial-by-501.
|
|
5735
|
+
permissionRules:
|
|
5736
|
+
type: boolean
|
|
5737
|
+
description: >
|
|
5738
|
+
The permission-rule lane is available: /v1/rules/cc-import/* , the respond persistRule redemption
|
|
5739
|
+
arm, and (from server 7.12.0) the GET/DELETE /v1/rules revocation face. Server predicate =
|
|
5740
|
+
PERMISSION_RULES_ENABLED AND a wired rule store — the SAME predicate behind every one of those
|
|
5741
|
+
endpoints' 501s, so "says yes" means all of the ones this server has actually work.
|
|
5742
|
+
🔴 THE KNOB'S DEFAULT READS IN TWO SEGMENTS: default OFF (opt-in) on server <= 7.11.0, default ON
|
|
5743
|
+
(opt-out) from 7.12.0, where the revocation face — the prerequisite that kept it opt-in — landed.
|
|
5744
|
+
So "the operator said nothing" means OPPOSITE things across that boundary; only an explicit
|
|
5745
|
+
`false` disables it. Read THIS BIT; never infer the lane's state from a knob default or a
|
|
5746
|
+
version number.
|
|
5747
|
+
permissionRulesRevoke:
|
|
5748
|
+
type: boolean
|
|
5749
|
+
description: >
|
|
5750
|
+
server >=7.12.0 — the rule REVOCATION face (GET + DELETE /v1/rules) is ROUTED on this worker.
|
|
5751
|
+
Same deps predicate as `permissionRules`; what carries the extra information is this key's mere
|
|
5752
|
+
PRESENCE. 🔴 That is the whole point of it being a second bit rather than a tightening of the
|
|
5753
|
+
first: on a <=7.11.0 worker `permissionRules` already answers true while these two routes 404, so
|
|
5754
|
+
that bit cannot distinguish "the lane is off" from "this build predates the face", and its
|
|
5755
|
+
published wording ("all four endpoints work") is already untrue there — you can add a bit, you
|
|
5756
|
+
cannot retroactively change one. ABSENT => an older worker: expect 404 `not_found.route` from
|
|
5757
|
+
`rules.list` / `rules.revoke` and hide the governance affordance. PRESENT => the routes exist
|
|
5758
|
+
(whether they then answer depends on `permissionRules`, as always).
|
|
5759
|
+
modeShellGateTranslation:
|
|
5760
|
+
type: boolean
|
|
5761
|
+
description: >
|
|
5762
|
+
design/201 §3 (server >=7.12.0) — this binary CONTAINS the `permissionMode` -> `spec.shellGate`
|
|
5763
|
+
translation table (bypassPermissions => off; auto/default/acceptEdits/plan => classify; no stated
|
|
5764
|
+
mode => the key is not written). HARD-CODED true and landed in the SAME COMMIT as the table, so
|
|
5765
|
+
"says yes <=> the translation is really there" is structural, not deps-derived — no missing
|
|
5766
|
+
dependency can make it false.
|
|
5767
|
+
🔴 THE PROBE MUST BE THIS BIT ON THE SERVER YOU ARE TALKING TO, NEVER YOUR OWN SHELL VERSION. A
|
|
5768
|
+
shell drops its unconditional `MANUAL_MODE_SHELL_GATE` env injection on the strength of this bit;
|
|
5769
|
+
keyed off the shell's own version instead, the three real deployments — new shell against an old
|
|
5770
|
+
server, a replica rolled back to an older binary, a mixed-version fleet — each open a window where
|
|
5771
|
+
the shell has stopped injecting and the server cannot yet translate. Absent/false ⇒ keep injecting.
|
|
5772
|
+
oneShot:
|
|
5773
|
+
type: boolean
|
|
5774
|
+
description: >
|
|
5775
|
+
server >=7.12.0 — this worker actually CONSUMES `TaskRequest.oneShot` (projects it into `TaskSpec`
|
|
5776
|
+
when it is a boolean, omits the key when absent, and 400s a non-boolean). Hard-coded true: the
|
|
5777
|
+
consumption depends on no optional facility. 🔴 PROBE IT BEFORE RELYING ON THE FIELD: the task
|
|
5778
|
+
request body is an OPEN set, so a worker that does not consume the key still accepts it and
|
|
5779
|
+
answers 200 — a headless caller would declare itself one-shot, never receive core's block-wait
|
|
5780
|
+
guidance, and lose its background results with nothing on the wire to reveal it. ABSENT => the key
|
|
5781
|
+
is being ignored; keep your own wait/reconcile step. (That step remains good practice even when
|
|
5782
|
+
the bit is present: the field is guidance to the model, not a completion contract.)
|
|
5783
|
+
outcomeLedger:
|
|
5784
|
+
type: boolean
|
|
5785
|
+
description: >
|
|
5786
|
+
server >=7.12.0 — the `GET /v1/outcomes` mechanical-signal read aggregate (design/73 §7.2) is
|
|
5787
|
+
available. Predicate mirrors the route's refusal arms exactly: a QUERYABLE ledger is wired (a
|
|
5788
|
+
file-only sink writes JSONL and has no query face ⇒ 501), AND on a multi-tenant deployment the
|
|
5789
|
+
operator list is non-empty (single-user has no such gate — the only user IS the operator).
|
|
5790
|
+
Added because this face previously had a 501 but NO capability bit, leaving consumers to
|
|
5791
|
+
trial-by-501 — the exact pattern this capability surface exists to remove.
|
|
5792
|
+
sharedMemory:
|
|
5793
|
+
type: boolean
|
|
5794
|
+
description: >
|
|
5795
|
+
design/177 org shared-memory read face (`GET /v1/shared-memory/*`) is mounted. Server-side the
|
|
5796
|
+
predicate is the SAME conjunction that mounts the route domain (SQL store present AND the
|
|
5797
|
+
principal→org membership fold present), so true ⟺ the face actually works — a deployment with
|
|
5798
|
+
the store but no membership authority deliberately reports false (and does not mount the routes).
|
|
5189
5799
|
workflows: { type: boolean, description: "ENGINE-CAN: this worker's engine can orchestrate S8 self-orchestration workflows (server: `workflowsCapable ?? Boolean(workflowRunStore)`). The MOUNTING of the durable /v1/workflows read routes is the separate `workflowsList` bit below; the two coincide in today's wiring but are declared as orthogonal axes." }
|
|
5190
5800
|
workflowsList: { type: boolean, description: "`GET /v1/workflows` list face (separate from the per-run reads)." }
|
|
5191
5801
|
resumeAt: { type: boolean, description: "E18 resume-at-message anchors are resolvable." }
|
|
@@ -7752,8 +8362,35 @@ components:
|
|
|
7752
8362
|
redeem it. The engine ALWAYS emits the section, so `false` is a REAL reading ("no rule lane on this
|
|
7753
8363
|
worker"), NOT "this engine is too old". The section is optional HERE only because this spec's support
|
|
7754
8364
|
floor is server 3.0.0, which predates it; absent = the worker never sent it.
|
|
8365
|
+
|
|
8366
|
+
🔴 `syncWired` / `orgGoverned` (core >=5.23.0 design/182 §9/§7, server >=7.12.0) sit in THIS
|
|
8367
|
+
section — the engine deliberately did not put them in the operator-only `governance` section, and
|
|
8368
|
+
the server projects all three bits on both faces. THEY WERE THE REASON THIS SECTION HAD TO BE
|
|
8369
|
+
REDECLARED: this arm is `additionalProperties: false`, so a strict consumer validating a real
|
|
8370
|
+
frame against the 6.13.0 spec did not "miss two fields" — the WHOLE SECTION failed validation and
|
|
8371
|
+
the diagnostics surface went unreadable. Same failure family as `errorCode` on `Event_tool_end`.
|
|
7755
8372
|
properties:
|
|
7756
8373
|
storeWired: { type: boolean }
|
|
8374
|
+
syncWired:
|
|
8375
|
+
type: boolean
|
|
8376
|
+
description: >
|
|
8377
|
+
core >=5.23.0 (design/182 §9) — this worker merges persisted rules into the SAME consent trust
|
|
8378
|
+
domain as its cross-device / transport / server-side peers. Read with `storeWired` to answer
|
|
8379
|
+
"how far does this 'don't ask again' actually travel?": the same click means something wider
|
|
8380
|
+
here than on a worker that keeps its rules local.
|
|
8381
|
+
🔴 PRESENT-AND-FALSE IS A REAL READING — "this engine has the concept, this deployment has not
|
|
8382
|
+
wired it" — NOT "too old to know". Absent (the whole key missing) is the only "did not send it"
|
|
8383
|
+
signal. On server 7.12.0 this is honestly `false` everywhere: the seam exists in core, the
|
|
8384
|
+
server has not wired a supplier yet.
|
|
8385
|
+
orgGoverned:
|
|
8386
|
+
type: boolean
|
|
8387
|
+
description: >
|
|
8388
|
+
core >=5.23.0 (design/182 §7) — an ORGANIZATION governance layer is armed over this rule lane.
|
|
8389
|
+
Load-bearing consequence: that layer is FAIL-CLOSED, so when the org snapshot cannot be read,
|
|
8390
|
+
EVERY allow is escalated into a real human approval rather than quietly honored. A shell that
|
|
8391
|
+
renders "don't ask again" without reading this bit is advertising an outcome it cannot promise.
|
|
8392
|
+
🔴 Same three-way reading as `syncWired`: present-and-false = concept known, not armed here;
|
|
8393
|
+
absent = the worker never sent the key. Honestly `false` on server 7.12.0.
|
|
7757
8394
|
governance:
|
|
7758
8395
|
type: object
|
|
7759
8396
|
additionalProperties: false
|
|
@@ -8150,6 +8787,44 @@ components:
|
|
|
8150
8787
|
governance shell gate sits at the "classify" tier, a shell ask may come from the classifier or from
|
|
8151
8788
|
another gate; the server leaves the key absent rather than guessing). Never render absence as
|
|
8152
8789
|
"this gate can be bypassed by a posture".
|
|
8790
|
+
ruleSuggestions:
|
|
8791
|
+
type: array
|
|
8792
|
+
minItems: 1 # 空数组不可能出现:server 的合取项②要求 `req.ruleSuggestions` **非空**,否则整键省略(缺席 = 本卡无此选项)
|
|
8793
|
+
# 刻意**不**声明 maxItems:server 的 `.max(4)` 是**卡** schema(`ApprovalCardSchema`)上的约束,这条
|
|
8794
|
+
# live 帧腿没有它。在这里抄一个上界 = 声明一条 server 并不执行的约束(照 spec 生成的严格客户端会
|
|
8795
|
+
# 拒收一条合法帧)。数量上界的真源是引擎:core 5.22.0 的 `suggestRulesForCommand` **至多铸一条,且恒
|
|
8796
|
+
# `match: "exact"`**(不写死上界是给将来的 miner 留位,不是暗示今天会有更多)。
|
|
8797
|
+
items: { $ref: '#/components/schemas/RuleSuggestion' }
|
|
8798
|
+
description: >
|
|
8799
|
+
server #154 车二 (core 5.18.0, design/179), ADDITIVE, "tool_approval" only: the rule candidates
|
|
8800
|
+
the ENGINE minted for this ask. Render them as the "don't ask again" options and echo the chosen
|
|
8801
|
+
one back as `persistRule.rule` on POST /v1/tool-approvals/{id}/respond.
|
|
8802
|
+
|
|
8803
|
+
🔴 PRESENCE IS THE PROMISE: the key is present ONLY on a deployment that actually has a
|
|
8804
|
+
permission-rule store wired (the same object behind the engine's `permissionRules.storeWired`
|
|
8805
|
+
manifest bit). A deployment without one OMITS it rather than offering an option with nowhere to
|
|
8806
|
+
redeem — an unredeemable "don't ask again" is a wire lie, worse than absence.
|
|
8807
|
+
|
|
8808
|
+
🔴 THE CONVERSE DOES NOT HOLD: absence means "THIS CARD has no redeemable candidate", and three
|
|
8809
|
+
causes produce it — no rule store wired, the engine minted no candidate for this command (compound /
|
|
8810
|
+
redirect / substitution), or the command bytes were unreadable. One frame cannot tell them apart, so
|
|
8811
|
+
never diagnose the deployment's rule lane from a single absence, and never synthesize a candidate.
|
|
8812
|
+
|
|
8813
|
+
📏 HOW MANY, TODAY: core 5.22.0's miner (`suggestRulesForCommand`) emits AT MOST ONE candidate and
|
|
8814
|
+
always `match: "exact"` — no shipped producer mints the broader prefix option. `prefix` stays in the
|
|
8815
|
+
closed vocabulary because the wire validator accepts it and a later miner may emit it; render what
|
|
8816
|
+
arrives, but do NOT build a UI that assumes a narrow/broad PAIR is on offer.
|
|
8817
|
+
|
|
8818
|
+
🔴 The values are the engine's VERBATIM (closed `match` vocabulary + engine-minted text); the
|
|
8819
|
+
server neither re-mints nor re-orders them. A hand-built rule string is refused at redemption
|
|
8820
|
+
(`ruleRefusal: "rule_not_offered"`) — by construction, so a click on a harmless command cannot be
|
|
8821
|
+
turned into a rule for something else.
|
|
8822
|
+
|
|
8823
|
+
⚠️ REDEMPTION SCOPE: the key also rides the durable card replay, but its only redemption channel is
|
|
8824
|
+
the LIVE respond leg keyed by this frame's `approvalId` — an in-process map on ONE replica. Same
|
|
8825
|
+
replica ⇒ redeemable; after a restart / on another replica that `approvalId` 404s and the person
|
|
8826
|
+
must use the durable decide leg, which carries NO rule field in v1 (its body is strict — an extra
|
|
8827
|
+
`persistRule` is a loud 400, not a silent drop).
|
|
8153
8828
|
fromSubagent:
|
|
8154
8829
|
type: boolean
|
|
8155
8830
|
enum: [true]
|
|
@@ -8269,11 +8944,91 @@ components:
|
|
|
8269
8944
|
posture. Semantics, the ABSENT != false clause and the discrimination boundary are stated verbatim on
|
|
8270
8945
|
ToolApprovalFrame.governanceForced. It sits at the card's top level rather than inside `risk` because
|
|
8271
8946
|
`risk` describes the ENGINE's judgement of the call, while this key says WHO imposed the gate.
|
|
8947
|
+
ruleSuggestions:
|
|
8948
|
+
type: array
|
|
8949
|
+
minItems: 1 # 同帧腿:空数组不可能出现(素材同源),缺席才是「本卡无此选项」
|
|
8950
|
+
maxItems: 4 # 卡形上**真有**这条上界(server `ApprovalCardSchema` 的 `.max(4)`);live 帧腿没有,故只写在这里
|
|
8951
|
+
items: { $ref: '#/components/schemas/RuleSuggestion' }
|
|
8952
|
+
description: >
|
|
8953
|
+
ADDITIVE (#154 车二 / core 5.18.0 design/179): the rule candidates the ENGINE minted for this ask —
|
|
8954
|
+
the card's "don't ask again" options; echo the chosen one back as `persistRule.rule`. Semantics,
|
|
8955
|
+
the PRESENCE-IS-THE-PROMISE clause (present only where a rule store is wired) and the redemption
|
|
8956
|
+
scope are stated verbatim on ToolApprovalFrame.ruleSuggestions — the live frame, the stored
|
|
8957
|
+
`card_json` and the replayed frame carry the SAME material.
|
|
8272
8958
|
fromSubagent: { type: boolean, enum: [true], description: 'Present ⇔ the ask comes from a DELEGATED child — the explicit discriminator (a worker cannot forge it).' }
|
|
8273
8959
|
sourceTaskId: { type: string, description: 'The child''s core session id — row-join value + fallback discriminator.' }
|
|
8274
8960
|
sourceAgentName: { type: string, description: 'The child agent''s display name, redacted. UNTRUSTED.' }
|
|
8275
8961
|
delegation: { $ref: '#/components/schemas/ApprovalCardDelegation' }
|
|
8276
8962
|
|
|
8963
|
+
RuleSuggestion:
|
|
8964
|
+
type: object
|
|
8965
|
+
description: >
|
|
8966
|
+
One ENGINE-minted rule candidate (#154 车二 / core 5.18.0 `RuleSuggestion`) — what a single click on
|
|
8967
|
+
"don't ask again" would persist.
|
|
8968
|
+
|
|
8969
|
+
🔴 THE TEXT COMES FROM THE ENGINE, NOT THE SHELL: the server derives it from the adjudicated command's
|
|
8970
|
+
own bytes; a client may only echo one of these back verbatim (`persistRule.rule`). Reporting text that
|
|
8971
|
+
is not among the candidates is refused (`ruleRefusal: "rule_not_offered"`) rather than guessed at, so
|
|
8972
|
+
"click on a harmless command, get a rule for something else" is not spellable on this path.
|
|
8973
|
+
# 封闭:core `RuleSuggestion` 恰三键(rule/match/command),server 逐字透传不重铸。
|
|
8974
|
+
additionalProperties: false
|
|
8975
|
+
required: [rule, match, command]
|
|
8976
|
+
properties:
|
|
8977
|
+
rule: { type: string, maxLength: 512, description: 'The canonical rule text to echo back on redemption (server MAX_RULE_TEXT_CHARS = 512).' }
|
|
8978
|
+
match:
|
|
8979
|
+
type: string
|
|
8980
|
+
enum: [exact, prefix]
|
|
8981
|
+
description: '`exact` = this one command only (`Bash(git status)`); `prefix` = word-boundary prefix (`Bash(git status:*)`). Closed vocabulary — an unknown value is not a candidate this contract describes; do not render it as redeemable.'
|
|
8982
|
+
command: { type: string, maxLength: 512, description: 'The command pattern inside the rule, for a shell that would rather not re-parse `rule`.' }
|
|
8983
|
+
|
|
8984
|
+
RuleRefusalReason:
|
|
8985
|
+
type: string
|
|
8986
|
+
description: >
|
|
8987
|
+
Why a "don't ask again" did NOT get persisted (#154 车二; the server's `RuleRefusalReason`, verbatim).
|
|
8988
|
+
A CLOSED set so a shell can branch instead of matching prose. The two naming styles are NOT a typo:
|
|
8989
|
+
the four hyphenated members are derived from the consent lane's own refusal reasons, the four
|
|
8990
|
+
underscored ones are minted by the server's gates.
|
|
8991
|
+
|
|
8992
|
+
🔴 A refusal NEVER flips the decision. The person's "allow this once" has already taken effect and
|
|
8993
|
+
reached the engine; turning the whole respond into a 4xx would make a real allow vanish. The honest
|
|
8994
|
+
shape is always 200 + `rulePersisted: false` + this reason.
|
|
8995
|
+
|
|
8996
|
+
`no-candidates` — the engine could mint no candidate for this command (compound/redirect/substitution).
|
|
8997
|
+
`unknown-candidate` — the echoed text does not locate in the record's candidate table.
|
|
8998
|
+
`confirm-refused` / `redeem-refused` — the consent protocol's confirm/redeem step was refused
|
|
8999
|
+
(record state, a lost CAS, or the store refusing the write).
|
|
9000
|
+
`rule_lane_unavailable` — FOUR causes share this one value (the server's three conjuncts): no rule store
|
|
9001
|
+
wired, OR the engine minted no candidate for this command, OR the command bytes were unreadable, OR this
|
|
9002
|
+
ask has no verified owner. 🔴 Two of those are per-CARD limits, so this reason is NOT evidence about the
|
|
9003
|
+
deployment — never disable the rule feature globally on the strength of it.
|
|
9004
|
+
`rule_input_edited` — the respond ALSO carried `updatedInput` (ctrl+g edit): the call that takes effect
|
|
9005
|
+
is the edited one while the candidates were minted from the ORIGINAL command, so persisting would arm a
|
|
9006
|
+
standing allow for the original (a real privilege widening). v1 refuses the persistence; the decision
|
|
9007
|
+
itself still stands.
|
|
9008
|
+
`rule_not_offered` — the echoed text is not among THIS ask's candidates (a stale card, or someone
|
|
9009
|
+
trying to mint a rule of their own).
|
|
9010
|
+
`rule_store_error` — the rule store wobbled. The adjudication is unaffected.
|
|
9011
|
+
`rule_governance_forced` (server >=7.12.0, #204) — the gate on THIS ask came from the deployment's
|
|
9012
|
+
operator governance layer, so the ask never entered the rule lane at all.
|
|
9013
|
+
🔴 SPLIT FROM `rule_lane_unavailable` ON PURPOSE: that one is "one of four conjuncts failed" (no rule
|
|
9014
|
+
store, no candidate, unreadable command bytes, no verified owner — two of which are per-CARD limits),
|
|
9015
|
+
this one is "the lane is perfectly healthy, only THIS ask is operator-governed". Folding them would
|
|
9016
|
+
erase a real distinction on the card.
|
|
9017
|
+
⚠️ NEITHER IS A DEPLOYMENT-LEVEL VERDICT. Every member of this enum describes ONE ask's outcome; the
|
|
9018
|
+
note on `rule_lane_unavailable` above already says never to disable the feature globally on it, and
|
|
9019
|
+
the same holds here. The one correct source for "does this worker serve rules at all" is
|
|
9020
|
+
`capabilities.permissionRules`. Keep offering "don't ask again" on the next ask; just not on this one.
|
|
9021
|
+
enum:
|
|
9022
|
+
- no-candidates
|
|
9023
|
+
- unknown-candidate
|
|
9024
|
+
- confirm-refused
|
|
9025
|
+
- redeem-refused
|
|
9026
|
+
- rule_lane_unavailable
|
|
9027
|
+
- rule_input_edited
|
|
9028
|
+
- rule_not_offered
|
|
9029
|
+
- rule_store_error
|
|
9030
|
+
- rule_governance_forced
|
|
9031
|
+
|
|
8277
9032
|
ApprovalRiskAxes:
|
|
8278
9033
|
type: object
|
|
8279
9034
|
description: >
|
|
@@ -9587,7 +10342,10 @@ components:
|
|
|
9587
10342
|
shape, `decision` echoed). Owner-gated with a 404 and NO existence oracle: non-owner and
|
|
9588
10343
|
unknown/settled/expired/wrong-replica ids are indistinguishable.
|
|
9589
10344
|
# 封闭(census 批2 第三段,2026-07-30):铸造点 `tool-approval.ts` 三恒在键 + 两条件键
|
|
9590
|
-
# (rememberApplied/updatedInputForwarded 的条件展开就在同一行字面量里)
|
|
10345
|
+
# (rememberApplied/updatedInputForwarded 的条件展开就在同一行字面量里)。
|
|
10346
|
+
# #154 车二(2026-08-10):`persistRuleAfterDecision` 的 `withFlag()` 在同一个体上再盖两个条件键
|
|
10347
|
+
# (rulePersisted 恒在、ruleRefusal 仅失败时)—— 键集从 5 升 7,**封闭性不动**(additive 不许把
|
|
10348
|
+
# census 闭合的口悄悄捅开)。
|
|
9591
10349
|
required: [approvalId, delivery, decision]
|
|
9592
10350
|
additionalProperties: false
|
|
9593
10351
|
properties:
|
|
@@ -9606,6 +10364,24 @@ components:
|
|
|
9606
10364
|
server 1.241 ([1458]): present (true) when an allow-family decision carried `updatedInput` and
|
|
9607
10365
|
the server forwarded the edited args to the engine — forwarded, not necessarily applied
|
|
9608
10366
|
(consumption depends on the core OnAsk object arm). Omitted on deny / no edit / older servers.
|
|
10367
|
+
rulePersisted:
|
|
10368
|
+
type: boolean
|
|
10369
|
+
description: >
|
|
10370
|
+
server #154 车二, ADDITIVE: whether the `persistRule` this respond carried actually landed in the
|
|
10371
|
+
rule store.
|
|
10372
|
+
|
|
10373
|
+
🔴 A FAILED PERSISTENCE NEVER FLIPS THE DECISION — the person's "allow this once" already took
|
|
10374
|
+
effect and reached the engine, so the honest shape is 200 + `rulePersisted: false` + `ruleRefusal`,
|
|
10375
|
+
NOT a 4xx. Render it as "allowed, but 'don't ask again' was not saved" — never as a failed approval.
|
|
10376
|
+
Omitted when the respond carried no `persistRule`, and on older servers; ABSENT is not `false`.
|
|
10377
|
+
ruleRefusal:
|
|
10378
|
+
allOf: [{ $ref: '#/components/schemas/RuleRefusalReason' }]
|
|
10379
|
+
description: >
|
|
10380
|
+
server #154 车二, ADDITIVE: the MACHINE-READABLE reason when `rulePersisted` is false (closed
|
|
10381
|
+
vocabulary). Always absent when `rulePersisted` is true. Branch on it to separate "try again on the
|
|
10382
|
+
next ask" (`rule_input_edited`) from "there was nothing redeemable on this card"
|
|
10383
|
+
(`rule_lane_unavailable`). 🔴 `rule_lane_unavailable` covers four causes, two of them per-card — do
|
|
10384
|
+
NOT read it as "this deployment has no rule lane" (see the RuleRefusalReason schema).
|
|
9609
10385
|
|
|
9610
10386
|
UsageMetric:
|
|
9611
10387
|
type: string
|
|
@@ -9830,3 +10606,490 @@ components:
|
|
|
9830
10606
|
path: { type: string, description: 'The relPath — pass VERBATIM as ?path= on the file sub-route.' }
|
|
9831
10607
|
hash: { type: string, description: 'Content address (sha256) — enables cross-snapshot change highlighting ([1894]②).' }
|
|
9832
10608
|
size: { type: integer, description: 'Present only when the store has a sizes face (SQL backends).' }
|
|
10609
|
+
|
|
10610
|
+
# ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
10611
|
+
# #154 车二(design/179 §7)—— 持久权限规则的 CC settings 导入面。形按 server 真码 + core 原形亲读
|
|
10612
|
+
# (`src/http/routes/rules.ts` / `src/rules-consent.ts`;preview/result 两形是 core `ImportPreview` /
|
|
10613
|
+
# `ImportResult` 的原样透传)。
|
|
10614
|
+
# ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
10615
|
+
|
|
10616
|
+
CcImportSettingsLayer:
|
|
10617
|
+
type: string
|
|
10618
|
+
enum: [userSettings, projectSettings, localSettings]
|
|
10619
|
+
description: >
|
|
10620
|
+
The three user-editable CC settings layers this version reads (core `ImportedSettingsLayer`). The two
|
|
10621
|
+
layers it does NOT read are named in `CcImportPreview.uncovered` rather than being silently absent.
|
|
10622
|
+
|
|
10623
|
+
RuleScope:
|
|
10624
|
+
description: >
|
|
10625
|
+
Where a persisted rule applies (core `RuleScope`). `project.root` is the directory prefix that decides
|
|
10626
|
+
which cwds the rule is live under.
|
|
10627
|
+
oneOf:
|
|
10628
|
+
- type: object
|
|
10629
|
+
additionalProperties: false
|
|
10630
|
+
required: [kind]
|
|
10631
|
+
properties:
|
|
10632
|
+
kind: { type: string, enum: [global] }
|
|
10633
|
+
- type: object
|
|
10634
|
+
additionalProperties: false
|
|
10635
|
+
required: [kind, root]
|
|
10636
|
+
properties:
|
|
10637
|
+
kind: { type: string, enum: [project] }
|
|
10638
|
+
root: { type: string, minLength: 1, description: 'The canonical directory this rule is scoped to. NEVER empty — an empty prefix contains EVERY cwd, which would silently promote a project rule to a global one.' }
|
|
10639
|
+
|
|
10640
|
+
RuleCandidate:
|
|
10641
|
+
type: object
|
|
10642
|
+
description: 'One candidate rule: the canonical text plus the scope it would land in (core `RuleCandidate`).'
|
|
10643
|
+
additionalProperties: false
|
|
10644
|
+
required: [rule, scope]
|
|
10645
|
+
properties:
|
|
10646
|
+
rule: { type: string, maxLength: 512 }
|
|
10647
|
+
scope: { $ref: '#/components/schemas/RuleScope' }
|
|
10648
|
+
|
|
10649
|
+
CcImportSkippedRule:
|
|
10650
|
+
type: object
|
|
10651
|
+
description: >
|
|
10652
|
+
One entry that did NOT become a candidate.
|
|
10653
|
+
|
|
10654
|
+
🔴 `reason` IS HUMAN-READABLE PROSE, NOT A MACHINE CODE. Upstream produces exactly three shapes:
|
|
10655
|
+
a grammar/validation refusal is `"<RuleRejectCode>: <message>"` (code-PREFIXED prose, e.g.
|
|
10656
|
+
`"invalid.bare_interpreter_prefix: …"` — never the bare code); a layer whose JSON does not parse is
|
|
10657
|
+
`"settings file is not valid JSON (<error text>)"`; a non-string allow entry is
|
|
10658
|
+
`"settings entry is not a string"`. If you must classify, key on the prefix BEFORE the first `:` and
|
|
10659
|
+
handle the two unprefixed shapes — do not equality-match the whole string and do not expect a bare code.
|
|
10660
|
+
|
|
10661
|
+
⚠️ `rule` is not always a rule text either: on the unparseable-JSON arm it carries THAT LAYER'S PATH
|
|
10662
|
+
(no entry was parsed, so there is no rule to name). Do not assume a `Bash(...)` shape when rendering.
|
|
10663
|
+
additionalProperties: false
|
|
10664
|
+
required: [rule, reason]
|
|
10665
|
+
properties:
|
|
10666
|
+
rule: { type: string }
|
|
10667
|
+
reason: { type: string }
|
|
10668
|
+
|
|
10669
|
+
CcImportLayerReport:
|
|
10670
|
+
type: object
|
|
10671
|
+
description: >
|
|
10672
|
+
Per-layer read report inside a preview.
|
|
10673
|
+
|
|
10674
|
+
⚠️ ON THIS HTTP ROUTE `found` IS ALWAYS TRUE. Upstream only writes false when its injected file reader
|
|
10675
|
+
returns undefined or throws, and the server adapter hands back the request's own REQUIRED `content`
|
|
10676
|
+
field — so it can never be absent. Empty text, `{}`, and a file with no allow bucket all still report
|
|
10677
|
+
`found: true` (they simply yield no candidates). Do NOT use `found` as "did this layer contribute
|
|
10678
|
+
rules" — read `candidates` for that. The field is kept because it is part of core's preview shape
|
|
10679
|
+
(it only discriminates for a host whose reader touches a real filesystem).
|
|
10680
|
+
additionalProperties: false
|
|
10681
|
+
required: [path, layer, found]
|
|
10682
|
+
properties:
|
|
10683
|
+
path: { type: string }
|
|
10684
|
+
layer: { $ref: '#/components/schemas/CcImportSettingsLayer' }
|
|
10685
|
+
found: { type: boolean }
|
|
10686
|
+
|
|
10687
|
+
CcImportLayer:
|
|
10688
|
+
type: object
|
|
10689
|
+
description: >
|
|
10690
|
+
One settings layer submitted to rulesCcImportPrepare. The CONTENT is submitted by the caller because a
|
|
10691
|
+
cloud deployment has no access to the user's filesystem — that is the deployment shape, not a bypass:
|
|
10692
|
+
what is read is still only the allow bucket.
|
|
10693
|
+
|
|
10694
|
+
🔴 `root` MUST be non-empty (an empty prefix contains every cwd — see RuleScope). Its TRUTHFULNESS is
|
|
10695
|
+
deliberately NOT validated: the server has no such tree to check against, and pretending to validate
|
|
10696
|
+
would be a lie. It is the user's own label for "which cwds should this rule be live under".
|
|
10697
|
+
# 封闭:server 侧 zod 是 `.strict()`(把 `content` 拼成 `contents` 的客户端应当场 400,而不是拿到
|
|
10698
|
+
# 一份「零候选」的预览去纳闷)。
|
|
10699
|
+
additionalProperties: false
|
|
10700
|
+
required: [layer, path, root, content]
|
|
10701
|
+
properties:
|
|
10702
|
+
layer: { $ref: '#/components/schemas/CcImportSettingsLayer' }
|
|
10703
|
+
path: { type: string, minLength: 1, maxLength: 1024 }
|
|
10704
|
+
root: { type: string, minLength: 1, maxLength: 1024 }
|
|
10705
|
+
content: { type: string, maxLength: 16384, description: 'The layer file''s raw JSON text (16 KiB cap — a settings file is hand-written, not a dataset; the cap keeps this from becoming an upload channel).' }
|
|
10706
|
+
|
|
10707
|
+
CcImportPreview:
|
|
10708
|
+
type: object
|
|
10709
|
+
description: >
|
|
10710
|
+
What the import WOULD do, shown before anyone confirms (core `ImportPreview`).
|
|
10711
|
+
|
|
10712
|
+
`uncovered` is a TWO-KEY RECORD, not a list: the two layers this version does not read are named in the
|
|
10713
|
+
TYPE, so an empty list, a missing member or a duplicate is not expressible. A preview that reported only
|
|
10714
|
+
the layers it read would be claiming a completeness it does not have.
|
|
10715
|
+
additionalProperties: false
|
|
10716
|
+
required: [candidates, skipped, layers, uncovered]
|
|
10717
|
+
properties:
|
|
10718
|
+
candidates:
|
|
10719
|
+
type: array
|
|
10720
|
+
items: { $ref: '#/components/schemas/RuleCandidate' }
|
|
10721
|
+
description: 'What would be persisted, in redemption order.'
|
|
10722
|
+
skipped:
|
|
10723
|
+
type: array
|
|
10724
|
+
items: { $ref: '#/components/schemas/CcImportSkippedRule' }
|
|
10725
|
+
description: 'Entries that did not become candidates. `reason` is PROSE, not a code — see the schema.'
|
|
10726
|
+
layers:
|
|
10727
|
+
type: array
|
|
10728
|
+
items: { $ref: '#/components/schemas/CcImportLayerReport' }
|
|
10729
|
+
uncovered:
|
|
10730
|
+
type: object
|
|
10731
|
+
additionalProperties: false
|
|
10732
|
+
required: [flagSettings, policySettings]
|
|
10733
|
+
properties:
|
|
10734
|
+
flagSettings: { type: string, enum: [not-imported-v1] }
|
|
10735
|
+
policySettings: { type: string, enum: [not-imported-v1] }
|
|
10736
|
+
|
|
10737
|
+
CcImportPrepareResult:
|
|
10738
|
+
type: object
|
|
10739
|
+
description: >
|
|
10740
|
+
The 200 body of rulesCcImportPrepare. NO RULE is in the store yet — the writer is only reached by
|
|
10741
|
+
rulesCcImportRedeem. (A pending approval record and this ticket ARE durable already; see the endpoint
|
|
10742
|
+
description — prepare is not idempotent.)
|
|
10743
|
+
additionalProperties: false
|
|
10744
|
+
required: [preview, ticket, expiresAtMs]
|
|
10745
|
+
properties:
|
|
10746
|
+
preview: { $ref: '#/components/schemas/CcImportPreview' }
|
|
10747
|
+
ticket:
|
|
10748
|
+
type: string
|
|
10749
|
+
description: >
|
|
10750
|
+
The redemption ticket: principal-bound, expiring, atomically single-use, and payload-bound (it pins
|
|
10751
|
+
a digest of the candidate set at mint time, so tampering with the record and redeeming an old ticket
|
|
10752
|
+
fails an equality rather than relying on "only we can write the record"). NOT a cacheable credential;
|
|
10753
|
+
never hand it to another user.
|
|
10754
|
+
expiresAtMs: { type: integer, description: 'Absolute server-clock expiry (the window is 10 minutes — one interaction, not a session).' }
|
|
10755
|
+
|
|
10756
|
+
CcImportResult:
|
|
10757
|
+
type: object
|
|
10758
|
+
description: >
|
|
10759
|
+
What the import ACTUALLY did (core `ImportResult`) — a different moment and a different contract from
|
|
10760
|
+
the preview, because dedup, concurrency and redemption-time validation can each move an entry between
|
|
10761
|
+
the two.
|
|
10762
|
+
|
|
10763
|
+
⚠️ `skippedAtRedeem` is empty on every success arm of THIS lane: the server treats a non-empty one as
|
|
10764
|
+
INDETERMINATE, releases the claim and answers 503 `state.rule_import_retry` instead of returning a 200
|
|
10765
|
+
that quietly means "half of it did not land". The field stays on the shape because it is core's.
|
|
10766
|
+
additionalProperties: false
|
|
10767
|
+
required: [persisted, deduped, skippedAtRedeem, rev]
|
|
10768
|
+
properties:
|
|
10769
|
+
persisted:
|
|
10770
|
+
type: array
|
|
10771
|
+
items: { $ref: '#/components/schemas/RuleCandidate' }
|
|
10772
|
+
deduped:
|
|
10773
|
+
type: array
|
|
10774
|
+
items: { $ref: '#/components/schemas/RuleCandidate' }
|
|
10775
|
+
description: 'Already in the store — not a failure.'
|
|
10776
|
+
skippedAtRedeem:
|
|
10777
|
+
type: array
|
|
10778
|
+
items: { $ref: '#/components/schemas/CcImportSkippedRule' }
|
|
10779
|
+
rev: { type: integer, description: 'The rule store''s monotonic revision after the batch.' }
|
|
10780
|
+
|
|
10781
|
+
CcImportRedeemResult:
|
|
10782
|
+
type: object
|
|
10783
|
+
description: 'The 200 body of rulesCcImportRedeem.'
|
|
10784
|
+
additionalProperties: false
|
|
10785
|
+
required: [result]
|
|
10786
|
+
properties:
|
|
10787
|
+
result: { $ref: '#/components/schemas/CcImportResult' }
|
|
10788
|
+
|
|
10789
|
+
# ── design/203 §2 撤销面(server 7.12.0)—— 列举行 + 两口的 200 体 ────────────────────────────────
|
|
10790
|
+
# 行形逐字段照 server `src/rules-consent.ts` 的 `PersistedRuleWireRow`(它自己逐字段照 core 的
|
|
10791
|
+
# `PersistedAllowRule`,只把 `scope` 换成判别式串)。
|
|
10792
|
+
|
|
10793
|
+
RuleDot:
|
|
10794
|
+
type: object
|
|
10795
|
+
description: >
|
|
10796
|
+
The immutable causal identity of ONE add — a replica identity plus a monotonic counter (core
|
|
10797
|
+
`RuleDot`). Compared by IDENTITY, never by causal order: single dots minted on different replicas
|
|
10798
|
+
have no order to compare. Treat it as an opaque pair; it is not a timestamp and not a sort key.
|
|
10799
|
+
additionalProperties: false
|
|
10800
|
+
required: [actor, counter]
|
|
10801
|
+
properties:
|
|
10802
|
+
actor: { type: string, description: 'The replica identity that minted this add.' }
|
|
10803
|
+
counter: { type: integer, description: 'That replica''s monotonic counter at mint time. Monotonic PER ACTOR only — never comparable across actors.' }
|
|
10804
|
+
|
|
10805
|
+
RuleAddOrigin:
|
|
10806
|
+
type: string
|
|
10807
|
+
description: >
|
|
10808
|
+
Where ONE add came from (core `RuleAddOrigin`). Stored PER ADD and never folded across dots — a
|
|
10809
|
+
folded provenance could not follow a dot that is later deleted on its own.
|
|
10810
|
+
`user` = minted from an approval card ("don't ask again"); `imported-cc` = redeemed through the
|
|
10811
|
+
CC-settings import lane; `starter` = a deployment-seeded rule.
|
|
10812
|
+
Closed vocabulary on the wire; read it with a default arm anyway (the owner is the engine).
|
|
10813
|
+
enum: [user, imported-cc, starter]
|
|
10814
|
+
|
|
10815
|
+
RuleAdd:
|
|
10816
|
+
type: object
|
|
10817
|
+
description: >
|
|
10818
|
+
ONE add of one logical rule, carrying its own dot and its own provenance (core `RuleAdd`). A rule is
|
|
10819
|
+
LIVE iff at least one of its adds survives the tombstones, which is why this is a SET and not a
|
|
10820
|
+
scalar: the same `(rule, scope)` can be approved several times, from different origins, at different
|
|
10821
|
+
moments. Folding it to a single "addedAt" would make "did I import this or click it?" unanswerable on
|
|
10822
|
+
a governance surface — which is the one place it matters.
|
|
10823
|
+
additionalProperties: false
|
|
10824
|
+
required: [dot, origin, createdAt]
|
|
10825
|
+
properties:
|
|
10826
|
+
dot: { $ref: '#/components/schemas/RuleDot' }
|
|
10827
|
+
origin: { $ref: '#/components/schemas/RuleAddOrigin' }
|
|
10828
|
+
createdAt: { type: string, description: 'When this ADD was recorded (engine-stamped ISO-8601). It dates the add, NOT the rule — a rule with several adds has several of these.' }
|
|
10829
|
+
|
|
10830
|
+
PersistedRule:
|
|
10831
|
+
type: object
|
|
10832
|
+
description: >
|
|
10833
|
+
One LIVE persisted allow rule as `GET /v1/rules` puts it on the wire (server `PersistedRuleWireRow`).
|
|
10834
|
+
Tombstones are already folded — every row here is live at `RuleListResult.rev`.
|
|
10835
|
+
🔴 `scope` is the DISCRIMINANT STRING (`global` / `project:<root>`), NOT the `RuleScope` object the
|
|
10836
|
+
import lane speaks: this is the exact byte sequence the revoke body wants back, so echo it VERBATIM
|
|
10837
|
+
rather than re-serializing a parsed form (an equivalent-but-differently-spelled root matches nothing).
|
|
10838
|
+
The rule lane is ALLOW-only; deny/ask rules are the tightening direction and never appear here.
|
|
10839
|
+
additionalProperties: false
|
|
10840
|
+
required: [rule, scope, tool, match, command, adds]
|
|
10841
|
+
properties:
|
|
10842
|
+
rule: { type: string, description: 'Canonical rule text — `Bash(git status)` (exact) or `Bash(git status:*)` (prefix). Pass back VERBATIM to revoke.' }
|
|
10843
|
+
scope: { type: string, description: 'Scope discriminant string: `global`, or `project:<root>` with a NON-EMPTY root. Pass back VERBATIM to revoke.' }
|
|
10844
|
+
tool: { type: string, enum: [Bash], description: 'The one tool the v1 rule lane speaks for. The field exists so a later version can widen without a shape change — read it, do not assume it.' }
|
|
10845
|
+
match:
|
|
10846
|
+
type: string
|
|
10847
|
+
enum: [exact, prefix]
|
|
10848
|
+
description: '`exact` = this one command only; `prefix` = word-boundary prefix. (core reserves a `wildcard` form for v2 that no current version produces.)'
|
|
10849
|
+
command: { type: string, description: 'What the matcher actually compares against, parsed out of `rule` at mint time and stored so no consumer re-parses. Trust this + `match`, not your own parse of `rule`.' }
|
|
10850
|
+
adds:
|
|
10851
|
+
type: array
|
|
10852
|
+
description: 'Every LIVE add of this rule (see RuleAdd — a real set, deliberately not folded).'
|
|
10853
|
+
items: { $ref: '#/components/schemas/RuleAdd' }
|
|
10854
|
+
|
|
10855
|
+
RuleListResult:
|
|
10856
|
+
type: object
|
|
10857
|
+
description: >
|
|
10858
|
+
The 200 body of `rulesList` — one keyset page plus the revision it was read at.
|
|
10859
|
+
`nextCursor` ABSENT = this was the last page (there is no empty-string sentinel). `rev` is the rule
|
|
10860
|
+
bucket's OCC revision; the cursor is bound to it, so a page fetched with a stale cursor is REFUSED
|
|
10861
|
+
rather than silently restarted.
|
|
10862
|
+
additionalProperties: false
|
|
10863
|
+
required: [rules, rev]
|
|
10864
|
+
properties:
|
|
10865
|
+
rules:
|
|
10866
|
+
type: array
|
|
10867
|
+
description: 'Live rules, ordered `(scope, rule)` lexicographic — a server-decided deterministic order, since keyset paging depends on it.'
|
|
10868
|
+
items: { $ref: '#/components/schemas/PersistedRule' }
|
|
10869
|
+
rev: { type: integer, description: 'The rule bucket''s OCC revision at read time. Bucket-LOCAL: two tenants each holding one rule both read `rev: 1`, so it is not a global clock and not comparable across principals.' }
|
|
10870
|
+
nextCursor: { type: string, description: 'Opaque base64url cursor for the next page. ABSENT ⇒ no further pages. Pass back VERBATIM; do not parse or mint one.' }
|
|
10871
|
+
|
|
10872
|
+
RuleRevokeRequest:
|
|
10873
|
+
type: object
|
|
10874
|
+
description: >
|
|
10875
|
+
The `DELETE /v1/rules` body. A rule is addressed BY CONTENT — there is no id anywhere in this face.
|
|
10876
|
+
Echo `rule` and `scope` VERBATIM from the listing: both are compared as opaque bytes, so an
|
|
10877
|
+
equivalent-but-differently-spelled scope root simply matches nothing (and answers a cheerful `no-op`).
|
|
10878
|
+
# 封闭:server 侧 zod 是 `.strict()` —— 把 `scope` 拼成 `scopes` 的客户端应当场知道,而不是
|
|
10879
|
+
# 拿到一个「删掉了 global 那条」的意外结果。
|
|
10880
|
+
additionalProperties: false
|
|
10881
|
+
required: [rule, scope]
|
|
10882
|
+
properties:
|
|
10883
|
+
rule: { type: string, minLength: 1, maxLength: 1024, description: 'The canonical rule text, VERBATIM as `RuleListResult.rules[].rule` gave it.' }
|
|
10884
|
+
scope: { type: string, minLength: 1, maxLength: 1088, description: 'The scope discriminant string — `global` or `project:<root>` with a NON-EMPTY root (an empty root would match every cwd). VERBATIM as the listing gave it.' }
|
|
10885
|
+
principal: { type: string, minLength: 1, maxLength: 190, description: 'OPERATOR-ONLY cross-tenant revoke. Absent (or your own principal) = revoke your own rule.' }
|
|
10886
|
+
|
|
10887
|
+
RuleRevokeResult:
|
|
10888
|
+
type: object
|
|
10889
|
+
description: >
|
|
10890
|
+
The 200 body of `rulesRevoke`. `removed` = a tombstone was written; `no-op` = nothing matched (an
|
|
10891
|
+
idempotent repeat, or it was never there — deliberately NOT a 404, which would be an existence
|
|
10892
|
+
oracle). BOTH arms carry `stillLive`, and the key set is identical on purpose so one consumer branch
|
|
10893
|
+
reads both.
|
|
10894
|
+
additionalProperties: false
|
|
10895
|
+
required: [status, rev, stillLive]
|
|
10896
|
+
properties:
|
|
10897
|
+
status:
|
|
10898
|
+
type: string
|
|
10899
|
+
enum: [removed, "no-op"]
|
|
10900
|
+
description: 'Whether this call wrote a tombstone. `no-op` says nothing about whether the rule EVER existed — do not render it as "not found".'
|
|
10901
|
+
rev: { type: integer, description: 'The rule bucket''s OCC revision after the operation.' }
|
|
10902
|
+
stillLive:
|
|
10903
|
+
type: boolean
|
|
10904
|
+
description: >
|
|
10905
|
+
🔴 `true` = the rule is STILL LIVE despite this call — under add-wins, an approval recorded during
|
|
10906
|
+
the operation keeps it alive. NEVER tell a human "the rule is gone" without reading this. It rides
|
|
10907
|
+
the `no-op` arm too, because the engine answers `no-op` off its initial snapshot with no read-back,
|
|
10908
|
+
so a concurrent approval leaves the rule alive while the verb reports nothing to remove.
|
|
10909
|
+
|
|
10910
|
+
🔴 THE TWO TRUTH VALUES ARE NOT EQUALLY STRONG, AND THE ASYMMETRY IS IN THE ENGINE, NOT HERE.
|
|
10911
|
+
`true` is positive evidence: something was observed alive. `false` is only "no evidence it
|
|
10912
|
+
survived" — on the `removed` arm the engine performs the confirming read-back inside a `catch`
|
|
10913
|
+
that swallows a failure and reports `false` (core `removePersistedRule`), so a degraded store
|
|
10914
|
+
yields a byte-identical `{status:"removed", stillLive:false}` to a genuinely verified removal.
|
|
10915
|
+
The `no-op` arm does NOT share that hole: the server does that read-back itself and lets a failure
|
|
10916
|
+
surface as a 500 rather than invent an answer. So: act on `true`; when it MATTERS that a rule is
|
|
10917
|
+
really gone (an operator revoking a standing allow), confirm with `GET /v1/rules` instead of
|
|
10918
|
+
treating `false` as proof. Registered upstream — an indeterminate outcome should be its own state
|
|
10919
|
+
rather than folding into `false`.
|
|
10920
|
+
|
|
10921
|
+
# ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
10922
|
+
# design/183 —— 身份收编(form b:纯身份重绑)。形逐字段照 server `src/adoption/wire.ts` 的 zod
|
|
10923
|
+
# (那份 schema 自陈是「跨仓 wire 契约,下游按字段名逐字钉围栏」)。
|
|
10924
|
+
# ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
10925
|
+
|
|
10926
|
+
AdoptionRequest:
|
|
10927
|
+
type: object
|
|
10928
|
+
description: 'The POST /v1/adoption body.'
|
|
10929
|
+
additionalProperties: false # server 侧 `.strict()`:多余键是调用方错误,当场 400(不静默吞)
|
|
10930
|
+
required: [fromPrincipal, toPrincipal]
|
|
10931
|
+
properties:
|
|
10932
|
+
fromPrincipal: { type: string, minLength: 1, maxLength: 190, description: 'The old identity being adopted. Already bound to a DIFFERENT destination ⇒ 409 adoption.source_already_bound (a second adoption / cross-tenant transfer is deliberately out of scope).' }
|
|
10933
|
+
toPrincipal: { type: string, minLength: 1, maxLength: 190, description: 'The EXISTING identity to adopt into (this form mints no credentials). Equal to fromPrincipal ⇒ 400 — that is a mistaken request, not a no-op success, and minting a row would permanently burn that source''s UNIQUE slot.' }
|
|
10934
|
+
|
|
10935
|
+
AdoptionSource:
|
|
10936
|
+
type: object
|
|
10937
|
+
description: 'Where the adoption came from. Form b has exactly one shape — a named principal.'
|
|
10938
|
+
additionalProperties: false
|
|
10939
|
+
required: [kind, principal]
|
|
10940
|
+
properties:
|
|
10941
|
+
kind: { type: string, enum: [principal] }
|
|
10942
|
+
principal: { type: string, minLength: 1 }
|
|
10943
|
+
|
|
10944
|
+
AdoptionLegAction:
|
|
10945
|
+
type: string
|
|
10946
|
+
enum: [bucket-rebind, row-rewrite, carried, none, reset]
|
|
10947
|
+
description: 'The per-store migration verb (closed set; form b only ever emits the first two plus `none`).'
|
|
10948
|
+
|
|
10949
|
+
AdoptionLeg:
|
|
10950
|
+
type: object
|
|
10951
|
+
description: 'One migration leg''s receipt.'
|
|
10952
|
+
additionalProperties: false
|
|
10953
|
+
required: [store, action]
|
|
10954
|
+
properties:
|
|
10955
|
+
store: { type: string, minLength: 1, description: 'Stable leg id = `<table>#<identity column>` (the server schema''s real names — reconcile against it).' }
|
|
10956
|
+
action: { $ref: '#/components/schemas/AdoptionLegAction' }
|
|
10957
|
+
rows:
|
|
10958
|
+
type: integer
|
|
10959
|
+
minimum: 0
|
|
10960
|
+
description: >
|
|
10961
|
+
Rows this leg changed when it RAN. 🔴 NOT a per-request delta and NOT a late-sweep delta: legs are
|
|
10962
|
+
only ever exposed inside `immutableReport`, so on a terminal arc every re-run replays the FROZEN
|
|
10963
|
+
first-run counts (a non-zero value here says nothing about what the current request did). On a
|
|
10964
|
+
`stalled` receipt it is the current row's reading, which can still move.
|
|
10965
|
+
quarantined: { type: integer, minimum: 0 }
|
|
10966
|
+
|
|
10967
|
+
AdoptionConfigEntry:
|
|
10968
|
+
type: object
|
|
10969
|
+
description: >
|
|
10970
|
+
One affected DEPLOYMENT configuration. 🔴 `migrated` is always false on the server half: these live in
|
|
10971
|
+
OTHER deployment units (BFF / cli / env / core declarations) that the adoption cannot reach — and a
|
|
10972
|
+
configuration one cannot change must not be reported as changed. The list being present, plus
|
|
10973
|
+
`migrated:false`, plus an unsettled account, is together the evidence for "configuration does not
|
|
10974
|
+
migrate, stated honestly".
|
|
10975
|
+
additionalProperties: false
|
|
10976
|
+
required: [deployment, key, requiredValue, migrated, ack, witnessedAtMs]
|
|
10977
|
+
properties:
|
|
10978
|
+
deployment: { type: string, minLength: 1 }
|
|
10979
|
+
key: { type: string, minLength: 1 }
|
|
10980
|
+
requiredValue: { type: string }
|
|
10981
|
+
migrated: { type: boolean }
|
|
10982
|
+
ack: { type: boolean, description: 'An operator acknowledgement — RECORD ONLY, it does not settle the account (a hand-written boolean can settle an account into a false green).' }
|
|
10983
|
+
witnessedAtMs:
|
|
10984
|
+
type: [integer, "null"]
|
|
10985
|
+
description: 'When a consumer''s runtime typed receipt actually read `requiredValue` back. 🔴 `null` = not witnessed = NOT settled.'
|
|
10986
|
+
|
|
10987
|
+
AdoptionNotMigratedFace:
|
|
10988
|
+
type: string
|
|
10989
|
+
enum: [usage-window, cost-quota, rate-limit, approval-exemption-grantor, approval-ask-decision-actor, image-bake-requested-by]
|
|
10990
|
+
description: >
|
|
10991
|
+
FROZEN machine-readable vocabulary of the faces the server deliberately does not migrate. Renaming or
|
|
10992
|
+
removing a member is a wire break; adding one is a new behaviour face. Consumers assert POSITIVELY on
|
|
10993
|
+
these so that "not migrated by design" and "forgotten" stay distinguishable in a black box.
|
|
10994
|
+
|
|
10995
|
+
AdoptionNotMigrated:
|
|
10996
|
+
type: object
|
|
10997
|
+
description: 'One positive "by design, not migrated" declaration.'
|
|
10998
|
+
additionalProperties: false
|
|
10999
|
+
required: [face, ruling, reason]
|
|
11000
|
+
properties:
|
|
11001
|
+
face: { $ref: '#/components/schemas/AdoptionNotMigratedFace' }
|
|
11002
|
+
ruling: { type: string, enum: [D1, D2], description: 'design/183 §12 ruling id — D1 = quota windows restart and recount; D2 = a historical field is never rewritten.' }
|
|
11003
|
+
reason: { type: string, minLength: 1 }
|
|
11004
|
+
|
|
11005
|
+
AdoptionReport:
|
|
11006
|
+
type: object
|
|
11007
|
+
description: >
|
|
11008
|
+
The adoption report.
|
|
11009
|
+
|
|
11010
|
+
🔴 "IMMUTABLE" HOLDS ONLY ONCE `status` IS `adopted`. The frozen row is written once at the final phase
|
|
11011
|
+
and replayed byte-for-byte thereafter, never recomputed — that byte-identity IS the idempotency
|
|
11012
|
+
criterion. But a `stalled` receipt has no such row yet, so the server PROJECTS a same-shaped report
|
|
11013
|
+
from the CURRENT row (`atMs` = the row's updated-at, configs/legs = the current columns). That
|
|
11014
|
+
projection is provisional: the next read can differ in every field. Do NOT persist a `stalled`
|
|
11015
|
+
report as audit evidence — take the frozen one after `status === "adopted"`. The two share one shape
|
|
11016
|
+
by upstream ruling (there is only one envelope); the discriminator is `status`, not the field name.
|
|
11017
|
+
additionalProperties: false
|
|
11018
|
+
required: [adoptionId, form, from, toPrincipal, atMs, affectedDeploymentConfigs, legs, notMigratedByDesign]
|
|
11019
|
+
properties:
|
|
11020
|
+
adoptionId: { type: string, minLength: 1 }
|
|
11021
|
+
form: { type: string, enum: [b], description: 'This version only does form b (pure identity rebind). Present explicitly so a later form-a receipt is distinguishable without inferring from an absent key.' }
|
|
11022
|
+
from: { $ref: '#/components/schemas/AdoptionSource' }
|
|
11023
|
+
toPrincipal: { type: string, minLength: 1 }
|
|
11024
|
+
atMs: { type: integer, minimum: 0 }
|
|
11025
|
+
affectedDeploymentConfigs:
|
|
11026
|
+
type: array
|
|
11027
|
+
items: { $ref: '#/components/schemas/AdoptionConfigEntry' }
|
|
11028
|
+
legs:
|
|
11029
|
+
type: array
|
|
11030
|
+
items: { $ref: '#/components/schemas/AdoptionLeg' }
|
|
11031
|
+
notMigratedByDesign:
|
|
11032
|
+
type: array
|
|
11033
|
+
items: { $ref: '#/components/schemas/AdoptionNotMigrated' }
|
|
11034
|
+
|
|
11035
|
+
AdoptionCurrent:
|
|
11036
|
+
type: object
|
|
11037
|
+
description: 'Recomputed on every read (the counterpart to AdoptionReport''s frozen history — do not infer one from the other).'
|
|
11038
|
+
additionalProperties: false
|
|
11039
|
+
required: [outstandingConfigs, quarantined, residualSourceRows]
|
|
11040
|
+
properties:
|
|
11041
|
+
outstandingConfigs:
|
|
11042
|
+
type: array
|
|
11043
|
+
items: { type: string }
|
|
11044
|
+
description: 'Config entries whose `witnessedAtMs` is null, as `<deployment>:<key>`. Empty = the account is settled.'
|
|
11045
|
+
quarantined: { type: integer, minimum: 0, description: 'Always 0 for form b (zero carriage ⇒ no rejected rows). The field is present so the envelope does not change shape when form a arrives.' }
|
|
11046
|
+
residualSourceRows:
|
|
11047
|
+
type: integer
|
|
11048
|
+
minimum: 0
|
|
11049
|
+
description: >
|
|
11050
|
+
Rows STILL under the old identity, recomputed on every read. AGGREGATION: counted PER TABLE with
|
|
11051
|
+
the table's identity axes unioned, so a row carrying the old identity on two or three axes counts
|
|
11052
|
+
ONCE; the per-table counts are then summed. It is deliberately NOT a sum over legs — reconciling
|
|
11053
|
+
this number against per-leg `rows` will not add up.
|
|
11054
|
+
|
|
11055
|
+
🔴 Load-bearing: adoption does NOT fence ordinary writers ("stop the engine first" is an operator
|
|
11056
|
+
precondition, not a machine guarantee), so a caller still injecting the old identity keeps writing
|
|
11057
|
+
rows after the terminal state while the idempotent short-circuit keeps answering `adopted`. So `> 0`
|
|
11058
|
+
means "rows may still sit under the old identity, OR the count could not be taken" — either way, GO
|
|
11059
|
+
LOOK. It is NOT proof that the deployment is actively writing. A POST also sweeps those late rows in
|
|
11060
|
+
and re-reports this; a GET does not sweep (though it does resume an in-flight arc — see the GET
|
|
11061
|
+
description).
|
|
11062
|
+
|
|
11063
|
+
⚠️ `1` IS AMBIGUOUS. When the count itself fails (query error / parse failure) the server reports
|
|
11064
|
+
`1` fail-closed rather than `0`, because `0` is the literal assertion "everything is clean" and it
|
|
11065
|
+
has not earned the right to say that. So `1` may mean "one residual row" OR "could not measure";
|
|
11066
|
+
the wire cannot tell them apart (the server log carries `adoption_residual_count_failed`). Use it
|
|
11067
|
+
as intended — `> 0` ⇒ GO LOOK — but never feed `=== 1` to a report as the exact number of rows.
|
|
11068
|
+
|
|
11069
|
+
AdoptionReceipt:
|
|
11070
|
+
type: object
|
|
11071
|
+
description: >
|
|
11072
|
+
The ONE adoption receipt envelope — the same SHAPE for the first success and for every idempotent
|
|
11073
|
+
re-run. No second receipt shape exists anywhere.
|
|
11074
|
+
|
|
11075
|
+
⚠️ Same shape carries NO byte-identity guarantee: `current` is recomputed on every call (residual
|
|
11076
|
+
count, config ledger) so two reads may match or may differ, and before the terminal state
|
|
11077
|
+
`immutableReport` is itself a provisional projection. The one thing guaranteed frozen is a TERMINAL
|
|
11078
|
+
`immutableReport`.
|
|
11079
|
+
|
|
11080
|
+
🔴 `adoptionId` is at the TOP LEVEL (the server sends `{adoptionId, ...receipt}`), carrying the same
|
|
11081
|
+
value as `immutableReport.adoptionId`.
|
|
11082
|
+
🔴 `status` is a TWO-WORD closed set. `rejected` is deliberately NOT a member: a refused adoption never
|
|
11083
|
+
produces a receipt, it answers a typed 409 — otherwise "refused" and "succeeded" would share one 200
|
|
11084
|
+
envelope and a consumer could only guess by reading fields.
|
|
11085
|
+
🔴 `status` is ALSO the precondition for reading `immutableReport`: it is frozen only once `adopted`;
|
|
11086
|
+
on `stalled` it is a provisional projection of the current row (see AdoptionReport).
|
|
11087
|
+
additionalProperties: false
|
|
11088
|
+
required: [adoptionId, status, immutableReport, current]
|
|
11089
|
+
properties:
|
|
11090
|
+
adoptionId: { type: string, minLength: 1 }
|
|
11091
|
+
status: { type: string, enum: [adopted, stalled], description: '`adopted` = the terminal state is in place (and only then is `immutableReport` frozen); `stalled` = in flight (the lock is held by another replica, or a crashed run has not been resumed yet). Not a failure.' }
|
|
11092
|
+
immutableReport:
|
|
11093
|
+
allOf: [{ $ref: '#/components/schemas/AdoptionReport' }]
|
|
11094
|
+
description: 'Frozen ONLY when `status` is `adopted`; on `stalled` it is a provisional projection of the current row — do not persist it as audit evidence.'
|
|
11095
|
+
current: { $ref: '#/components/schemas/AdoptionCurrent' }
|