@sema-agent/sdk 6.11.0 → 6.13.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/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/index.d.ts +14 -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 +175 -0
- package/dist/resources/rules.d.ts.map +1 -0
- package/dist/resources/rules.js +40 -0
- package/dist/resources/rules.js.map +1 -0
- package/dist/resources/tool-approvals.d.ts +111 -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 +25 -0
- package/dist/types.d.ts.map +1 -1
- package/openapi.yaml +870 -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,242 @@ 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/183 —— 身份收编面(form b:纯身份重绑)。operator-only 的一次性部署级动作,零模型工作。 ──
|
|
4088
|
+
/v1/adoption:
|
|
4089
|
+
parameters:
|
|
4090
|
+
- $ref: '#/components/parameters/PrincipalHeader'
|
|
4091
|
+
post:
|
|
4092
|
+
tags: [adoption]
|
|
4093
|
+
operationId: adoptionStart
|
|
4094
|
+
x-status: gated # server routes/adoption.ts;需 SQL 后端(DB_BACKEND=mysql|pg),否则 501 capability.adoption_store_required。SDK adoption.start() 消费。
|
|
4095
|
+
summary: Adopt one principal's data under another principal (identity rebind, form b).
|
|
4096
|
+
description: >
|
|
4097
|
+
A one-shot DEPLOYMENT action, not a business surface: operator-only, nothing billed, and honestly 501
|
|
4098
|
+
on a deployment with no SQL backend (the identity axis being rebound does not exist on the local one).
|
|
4099
|
+
|
|
4100
|
+
IDEMPOTENT: re-running with the same parameters never starts a second adoption, and ONCE THE ARC IS
|
|
4101
|
+
TERMINAL it replays the same frozen `immutableReport` byte-for-byte (written once at the final phase,
|
|
4102
|
+
never recomputed). The RECEIPT AS A WHOLE carries no byte-identity guarantee — `current` is recomputed
|
|
4103
|
+
on every call, so two reads may match or may differ — and before the terminal state
|
|
4104
|
+
(`status: "stalled"`) the report itself is a provisional projection of the current row. Concurrent identical POSTs are
|
|
4105
|
+
arbitrated by a UNIQUE on the source principal: exactly one row lands and both connections read the
|
|
4106
|
+
same `adoptionId`.
|
|
4107
|
+
|
|
4108
|
+
🔴 A REFUSED ADOPTION NEVER PRODUCES A RECEIPT. `status` is a two-word closed set
|
|
4109
|
+
(`adopted`/`stalled`) with NO `rejected` member — refusals are typed 409s. Otherwise "refused" and
|
|
4110
|
+
"succeeded" would share one 200 envelope and a consumer could only guess by reading fields.
|
|
4111
|
+
|
|
4112
|
+
🔴 This does NOT fence off ordinary writers. "Stop the engine first" is an OPERATOR PRECONDITION, not
|
|
4113
|
+
a machine guarantee: a caller still injecting the old identity keeps writing rows after the terminal
|
|
4114
|
+
state, while the idempotent short-circuit keeps answering `adopted`. That is what
|
|
4115
|
+
`current.residualSourceRows` makes visible — `> 0` means rows MAY still sit under the old identity OR
|
|
4116
|
+
the count itself failed (the server reports 1 fail-closed when it cannot count); either way, go look.
|
|
4117
|
+
It is not proof of an active writer. A POST (a write verb) also sweeps those late rows into the new
|
|
4118
|
+
identity and re-reports the count; a GET does not sweep — but it DOES resume an in-flight arc (see the
|
|
4119
|
+
GET description), so it is not a safe poll either.
|
|
4120
|
+
requestBody:
|
|
4121
|
+
required: true
|
|
4122
|
+
content:
|
|
4123
|
+
application/json:
|
|
4124
|
+
schema: { $ref: '#/components/schemas/AdoptionRequest' }
|
|
4125
|
+
responses:
|
|
4126
|
+
'200':
|
|
4127
|
+
description: >
|
|
4128
|
+
The adoption receipt — the SAME ENVELOPE SHAPE for the first success and for every idempotent
|
|
4129
|
+
re-run. There is NO byte-identity guarantee for the whole receipt (it may match or differ):
|
|
4130
|
+
`current` is recomputed on every call. The one frozen part is a TERMINAL `immutableReport`.
|
|
4131
|
+
content:
|
|
4132
|
+
application/json:
|
|
4133
|
+
schema: { $ref: '#/components/schemas/AdoptionReceipt' }
|
|
4134
|
+
'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
|
|
4135
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
4136
|
+
'403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only
|
|
4137
|
+
'409':
|
|
4138
|
+
description: >
|
|
4139
|
+
A refusal — THREE distinct errorCodes ride this status and the operator's next action differs for
|
|
4140
|
+
each, so branch on `errorCode`, never on the status:
|
|
4141
|
+
`adoption.source_already_bound` (this source is already bound to a DIFFERENT destination — a
|
|
4142
|
+
second adoption / cross-tenant transfer, deliberately out of scope; body carries `adoptionId`,
|
|
4143
|
+
`fromPrincipal`, `boundTo`);
|
|
4144
|
+
`adoption.destination_conflict` (the destination already holds rows sharing a logical key with
|
|
4145
|
+
the source; body lists the conflicting tables in `conflicts` — go clean those up);
|
|
4146
|
+
`adoption.destination_unrepresentable` (the destination identity does not fit some key this
|
|
4147
|
+
adoption would DERIVE from it, e.g. a memory `proj:` key overrunning a column width — pick a
|
|
4148
|
+
SHORTER destination principal).
|
|
4149
|
+
Either way both sides are byte-unchanged.
|
|
4150
|
+
content:
|
|
4151
|
+
application/json:
|
|
4152
|
+
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
|
4153
|
+
'501': { $ref: '#/components/responses/NotImplemented' } # capability.adoption_store_required — adoption requires a SQL store backend (DB_BACKEND=mysql|pg)
|
|
4154
|
+
|
|
4155
|
+
/v1/adoption/{adoptionId}:
|
|
4156
|
+
parameters:
|
|
4157
|
+
- $ref: '#/components/parameters/PrincipalHeader'
|
|
4158
|
+
- { name: adoptionId, in: path, required: true, schema: { type: string }, description: 'From AdoptionReceipt.adoptionId.' }
|
|
4159
|
+
get:
|
|
4160
|
+
tags: [adoption]
|
|
4161
|
+
operationId: adoptionGet
|
|
4162
|
+
x-status: gated # server routes/adoption.ts;同 POST 的部署谓词。SDK adoption.get() 消费。
|
|
4163
|
+
summary: Read one adoption's state (and RESUME it if it is still in flight).
|
|
4164
|
+
description: >
|
|
4165
|
+
🔴 THIS IS NOT A SIDE-EFFECT-FREE READ. On a TERMINAL row (adopted / refused) it is a pure read. On a
|
|
4166
|
+
row that is STILL IN FLIGHT — the typical case being a deployment where nothing has POSTed again since
|
|
4167
|
+
a crash — it RESUMES THE ARC: the same `driveUnderLock` the boot scan uses, advancing phases, rewriting
|
|
4168
|
+
rows on the identity axis, recording the config ledger and landing the terminal state. That is
|
|
4169
|
+
deliberate upstream (otherwise one crash would require a human to re-POST), but a consumer must know
|
|
4170
|
+
it: do NOT treat this as a safe prefetch / dashboard poll — reading an in-flight arc is pressing
|
|
4171
|
+
continue on it. The ONLY difference from the POST is that the GET does NOT sweep late rows into the
|
|
4172
|
+
new identity (the POST does). Repeating the call is safe (advisory lock + phase CAS + idempotent
|
|
4173
|
+
identity ⇒ a replay lands where one call lands) — but "safely replayable" is not "read-only". There
|
|
4174
|
+
is no verb on this wire today that guarantees zero progression.
|
|
4175
|
+
|
|
4176
|
+
`immutableReport` is historical fact — but only ONCE `status` is `adopted`; on a `stalled` receipt it is
|
|
4177
|
+
a provisional projection of the current row and can differ between two reads (see the AdoptionReport
|
|
4178
|
+
schema). `current` is recomputed every time. Do not infer one half from the other — in particular
|
|
4179
|
+
`status: "adopted"` does NOT mean the configuration migration is done; that account lives in
|
|
4180
|
+
`current.outstandingConfigs` / `current.residualSourceRows`, and a non-zero residual may also mean the
|
|
4181
|
+
count itself failed (see that field).
|
|
4182
|
+
responses:
|
|
4183
|
+
'200':
|
|
4184
|
+
description: The adoption receipt.
|
|
4185
|
+
content:
|
|
4186
|
+
application/json:
|
|
4187
|
+
schema: { $ref: '#/components/schemas/AdoptionReceipt' }
|
|
4188
|
+
'400': { $ref: '#/components/responses/BadRequest' } # request.id_invalid — undecodable id (shared with sessions/attachments; no second synonym code was minted)
|
|
4189
|
+
'401': { $ref: '#/components/responses/Unauthorized' }
|
|
4190
|
+
'403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only
|
|
4191
|
+
'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.
|
|
4192
|
+
'409':
|
|
4193
|
+
description: 'Same three refusal codes as adoptionStart (a refused adoption keeps answering its refusal).'
|
|
4194
|
+
content:
|
|
4195
|
+
application/json:
|
|
4196
|
+
schema: { $ref: '#/components/schemas/ErrorResponse' }
|
|
4197
|
+
'501': { $ref: '#/components/responses/NotImplemented' } # capability.adoption_store_required
|
|
4198
|
+
|
|
3858
4199
|
/metrics/summary:
|
|
3859
4200
|
get:
|
|
3860
4201
|
tags: [metrics]
|
|
@@ -4927,8 +5268,12 @@ components:
|
|
|
4927
5268
|
type: object
|
|
4928
5269
|
description: >
|
|
4929
5270
|
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
|
-
|
|
5271
|
+
the user/objective opening turn is NOT projected today). blocks is an OPEN set. tool-result.output IS
|
|
5272
|
+
present (server trace/project.ts projects it; the old "ALWAYS absent" note was stale — [3315] finding) —
|
|
5273
|
+
but note it carries the STORED form: outputs over the engine's offload threshold are persisted in
|
|
5274
|
+
bounded form (head/tail preview + `<persisted-output ref>` marker), so `output` here can be the
|
|
5275
|
+
truncated form, disclosed via `truncated`/`totalChars`. Full retrieval of an offloaded output awaits
|
|
5276
|
+
the tool-results read face ([3316] server half, planned).
|
|
4932
5277
|
required: [seq, ts, role, blocks]
|
|
4933
5278
|
additionalProperties: true
|
|
4934
5279
|
properties:
|
|
@@ -4968,6 +5313,9 @@ components:
|
|
|
4968
5313
|
# 于是「server 少发 retainedFrom」这种断页判据丢失对本门不可见。
|
|
4969
5314
|
additionalProperties: false
|
|
4970
5315
|
required: [turns, retainedFrom]
|
|
5316
|
+
# ⚠️ 路别差异契约([3315]② 定谳,core [3316] 判设计事实非缺陷):trace 记账只在 /v1/runs 路;
|
|
5317
|
+
# sync `POST /v1/tasks` 完成后的 turns 读面对该 run 恒 `turns: []`(retainedFrom: 0)。这是契约,
|
|
5318
|
+
# 不是待修缺口——sync 路要 trace 请改走 /v1/runs。哪天 sync 路开始填 turns = 行为变更需过本表。
|
|
4971
5319
|
properties:
|
|
4972
5320
|
turns:
|
|
4973
5321
|
type: array
|
|
@@ -5048,6 +5396,21 @@ components:
|
|
|
5048
5396
|
type: array
|
|
5049
5397
|
items: { $ref: '#/components/schemas/Artifact' }
|
|
5050
5398
|
|
|
5399
|
+
ToolResultSlice:
|
|
5400
|
+
type: object
|
|
5401
|
+
description: >
|
|
5402
|
+
One slice of an offloaded tool result (`GET /v1/tasks/{taskId}/tool-results/{ref}`) — the wire
|
|
5403
|
+
form of the engine's `ToolResultSlice`. `offset` echoes where this slice starts (so successive
|
|
5404
|
+
reads concatenate deterministically) and `totalChars` is the length of the FULL stored content,
|
|
5405
|
+
independent of the slice — a client pages until `offset + content.length === totalChars`.
|
|
5406
|
+
# 封闭:铸造点 routes/trace-usage.ts `handleToolResult` 只发这三键(core ToolResultSlice 原形)。
|
|
5407
|
+
additionalProperties: false
|
|
5408
|
+
required: [content, offset, totalChars]
|
|
5409
|
+
properties:
|
|
5410
|
+
content: { type: string, description: 'The slice text.' }
|
|
5411
|
+
offset: { type: integer, description: 'Char offset this slice starts at.' }
|
|
5412
|
+
totalChars: { type: integer, description: 'Total chars of the FULL stored content (not this slice).' }
|
|
5413
|
+
|
|
5051
5414
|
LeaderReceipt:
|
|
5052
5415
|
type: object
|
|
5053
5416
|
description: >
|
|
@@ -5186,6 +5549,25 @@ components:
|
|
|
5186
5549
|
legacy verbs in 1.0.0; the live face is `client.memory.exportScope` / `.sync`
|
|
5187
5550
|
(`GET /v1/memory/export`, `POST /v1/memory/sync/{scope}`), which this flag does NOT gate.
|
|
5188
5551
|
Kept on the wire so an older shell that still reads it honestly hides the dead affordances.
|
|
5552
|
+
adoption:
|
|
5553
|
+
type: boolean
|
|
5554
|
+
description: >
|
|
5555
|
+
design/183 form b adoption face (POST /v1/adoption + GET /v1/adoption/:id, operator lane) is
|
|
5556
|
+
wired. Same predicate as the routes' 501 (the SQL backend's adoptionLog factory is present;
|
|
5557
|
+
the local backend deliberately omits it). Probe before use, no trial-by-501.
|
|
5558
|
+
permissionRules:
|
|
5559
|
+
type: boolean
|
|
5560
|
+
description: >
|
|
5561
|
+
The permission-rule lane (/v1/rules/cc-import/* plus the respond persistRule redemption arm)
|
|
5562
|
+
is available. Server predicate = PERMISSION_RULES_ENABLED=true AND the rule store is wired
|
|
5563
|
+
(default OFF => false; the revocation face is a prerequisite for enabling).
|
|
5564
|
+
sharedMemory:
|
|
5565
|
+
type: boolean
|
|
5566
|
+
description: >
|
|
5567
|
+
design/177 org shared-memory read face (`GET /v1/shared-memory/*`) is mounted. Server-side the
|
|
5568
|
+
predicate is the SAME conjunction that mounts the route domain (SQL store present AND the
|
|
5569
|
+
principal→org membership fold present), so true ⟺ the face actually works — a deployment with
|
|
5570
|
+
the store but no membership authority deliberately reports false (and does not mount the routes).
|
|
5189
5571
|
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
5572
|
workflowsList: { type: boolean, description: "`GET /v1/workflows` list face (separate from the per-run reads)." }
|
|
5191
5573
|
resumeAt: { type: boolean, description: "E18 resume-at-message anchors are resolvable." }
|
|
@@ -6025,6 +6407,8 @@ components:
|
|
|
6025
6407
|
status: { type: string, description: 'the TaskResult''s terminal status (open vocabulary; core enum verbatim).' }
|
|
6026
6408
|
error: { type: string, description: 'first line of errorMessage, clipped to 300 chars (redacted). Present only when the result carried an errorMessage.' }
|
|
6027
6409
|
result: { type: string, description: 'the result text, clipped to 2000 chars (redacted). Present only when the result carried one.' }
|
|
6410
|
+
resultTruncated: { type: boolean, description: 'server >=7.9.0 (A-002.6): present (true) only when `result` was clipped at 2000 chars — the disclosure twin of the >64KiB whole-row truncated arm; absent = the result field is complete.' }
|
|
6411
|
+
resultChars: { type: integer, description: 'server >=7.9.0 (A-002.6): the ORIGINAL result length in characters; present only alongside resultTruncated.' }
|
|
6028
6412
|
tokens: { type: integer, description: 'present only when the result carried stats.' }
|
|
6029
6413
|
turns: { type: integer, description: 'present only when the result carried stats.' }
|
|
6030
6414
|
|
|
@@ -8148,6 +8532,44 @@ components:
|
|
|
8148
8532
|
governance shell gate sits at the "classify" tier, a shell ask may come from the classifier or from
|
|
8149
8533
|
another gate; the server leaves the key absent rather than guessing). Never render absence as
|
|
8150
8534
|
"this gate can be bypassed by a posture".
|
|
8535
|
+
ruleSuggestions:
|
|
8536
|
+
type: array
|
|
8537
|
+
minItems: 1 # 空数组不可能出现:server 的合取项②要求 `req.ruleSuggestions` **非空**,否则整键省略(缺席 = 本卡无此选项)
|
|
8538
|
+
# 刻意**不**声明 maxItems:server 的 `.max(4)` 是**卡** schema(`ApprovalCardSchema`)上的约束,这条
|
|
8539
|
+
# live 帧腿没有它。在这里抄一个上界 = 声明一条 server 并不执行的约束(照 spec 生成的严格客户端会
|
|
8540
|
+
# 拒收一条合法帧)。数量上界的真源是引擎:core 5.22.0 的 `suggestRulesForCommand` **至多铸一条,且恒
|
|
8541
|
+
# `match: "exact"`**(不写死上界是给将来的 miner 留位,不是暗示今天会有更多)。
|
|
8542
|
+
items: { $ref: '#/components/schemas/RuleSuggestion' }
|
|
8543
|
+
description: >
|
|
8544
|
+
server #154 车二 (core 5.18.0, design/179), ADDITIVE, "tool_approval" only: the rule candidates
|
|
8545
|
+
the ENGINE minted for this ask. Render them as the "don't ask again" options and echo the chosen
|
|
8546
|
+
one back as `persistRule.rule` on POST /v1/tool-approvals/{id}/respond.
|
|
8547
|
+
|
|
8548
|
+
🔴 PRESENCE IS THE PROMISE: the key is present ONLY on a deployment that actually has a
|
|
8549
|
+
permission-rule store wired (the same object behind the engine's `permissionRules.storeWired`
|
|
8550
|
+
manifest bit). A deployment without one OMITS it rather than offering an option with nowhere to
|
|
8551
|
+
redeem — an unredeemable "don't ask again" is a wire lie, worse than absence.
|
|
8552
|
+
|
|
8553
|
+
🔴 THE CONVERSE DOES NOT HOLD: absence means "THIS CARD has no redeemable candidate", and three
|
|
8554
|
+
causes produce it — no rule store wired, the engine minted no candidate for this command (compound /
|
|
8555
|
+
redirect / substitution), or the command bytes were unreadable. One frame cannot tell them apart, so
|
|
8556
|
+
never diagnose the deployment's rule lane from a single absence, and never synthesize a candidate.
|
|
8557
|
+
|
|
8558
|
+
📏 HOW MANY, TODAY: core 5.22.0's miner (`suggestRulesForCommand`) emits AT MOST ONE candidate and
|
|
8559
|
+
always `match: "exact"` — no shipped producer mints the broader prefix option. `prefix` stays in the
|
|
8560
|
+
closed vocabulary because the wire validator accepts it and a later miner may emit it; render what
|
|
8561
|
+
arrives, but do NOT build a UI that assumes a narrow/broad PAIR is on offer.
|
|
8562
|
+
|
|
8563
|
+
🔴 The values are the engine's VERBATIM (closed `match` vocabulary + engine-minted text); the
|
|
8564
|
+
server neither re-mints nor re-orders them. A hand-built rule string is refused at redemption
|
|
8565
|
+
(`ruleRefusal: "rule_not_offered"`) — by construction, so a click on a harmless command cannot be
|
|
8566
|
+
turned into a rule for something else.
|
|
8567
|
+
|
|
8568
|
+
⚠️ REDEMPTION SCOPE: the key also rides the durable card replay, but its only redemption channel is
|
|
8569
|
+
the LIVE respond leg keyed by this frame's `approvalId` — an in-process map on ONE replica. Same
|
|
8570
|
+
replica ⇒ redeemable; after a restart / on another replica that `approvalId` 404s and the person
|
|
8571
|
+
must use the durable decide leg, which carries NO rule field in v1 (its body is strict — an extra
|
|
8572
|
+
`persistRule` is a loud 400, not a silent drop).
|
|
8151
8573
|
fromSubagent:
|
|
8152
8574
|
type: boolean
|
|
8153
8575
|
enum: [true]
|
|
@@ -8267,11 +8689,80 @@ components:
|
|
|
8267
8689
|
posture. Semantics, the ABSENT != false clause and the discrimination boundary are stated verbatim on
|
|
8268
8690
|
ToolApprovalFrame.governanceForced. It sits at the card's top level rather than inside `risk` because
|
|
8269
8691
|
`risk` describes the ENGINE's judgement of the call, while this key says WHO imposed the gate.
|
|
8692
|
+
ruleSuggestions:
|
|
8693
|
+
type: array
|
|
8694
|
+
minItems: 1 # 同帧腿:空数组不可能出现(素材同源),缺席才是「本卡无此选项」
|
|
8695
|
+
maxItems: 4 # 卡形上**真有**这条上界(server `ApprovalCardSchema` 的 `.max(4)`);live 帧腿没有,故只写在这里
|
|
8696
|
+
items: { $ref: '#/components/schemas/RuleSuggestion' }
|
|
8697
|
+
description: >
|
|
8698
|
+
ADDITIVE (#154 车二 / core 5.18.0 design/179): the rule candidates the ENGINE minted for this ask —
|
|
8699
|
+
the card's "don't ask again" options; echo the chosen one back as `persistRule.rule`. Semantics,
|
|
8700
|
+
the PRESENCE-IS-THE-PROMISE clause (present only where a rule store is wired) and the redemption
|
|
8701
|
+
scope are stated verbatim on ToolApprovalFrame.ruleSuggestions — the live frame, the stored
|
|
8702
|
+
`card_json` and the replayed frame carry the SAME material.
|
|
8270
8703
|
fromSubagent: { type: boolean, enum: [true], description: 'Present ⇔ the ask comes from a DELEGATED child — the explicit discriminator (a worker cannot forge it).' }
|
|
8271
8704
|
sourceTaskId: { type: string, description: 'The child''s core session id — row-join value + fallback discriminator.' }
|
|
8272
8705
|
sourceAgentName: { type: string, description: 'The child agent''s display name, redacted. UNTRUSTED.' }
|
|
8273
8706
|
delegation: { $ref: '#/components/schemas/ApprovalCardDelegation' }
|
|
8274
8707
|
|
|
8708
|
+
RuleSuggestion:
|
|
8709
|
+
type: object
|
|
8710
|
+
description: >
|
|
8711
|
+
One ENGINE-minted rule candidate (#154 车二 / core 5.18.0 `RuleSuggestion`) — what a single click on
|
|
8712
|
+
"don't ask again" would persist.
|
|
8713
|
+
|
|
8714
|
+
🔴 THE TEXT COMES FROM THE ENGINE, NOT THE SHELL: the server derives it from the adjudicated command's
|
|
8715
|
+
own bytes; a client may only echo one of these back verbatim (`persistRule.rule`). Reporting text that
|
|
8716
|
+
is not among the candidates is refused (`ruleRefusal: "rule_not_offered"`) rather than guessed at, so
|
|
8717
|
+
"click on a harmless command, get a rule for something else" is not spellable on this path.
|
|
8718
|
+
# 封闭:core `RuleSuggestion` 恰三键(rule/match/command),server 逐字透传不重铸。
|
|
8719
|
+
additionalProperties: false
|
|
8720
|
+
required: [rule, match, command]
|
|
8721
|
+
properties:
|
|
8722
|
+
rule: { type: string, maxLength: 512, description: 'The canonical rule text to echo back on redemption (server MAX_RULE_TEXT_CHARS = 512).' }
|
|
8723
|
+
match:
|
|
8724
|
+
type: string
|
|
8725
|
+
enum: [exact, prefix]
|
|
8726
|
+
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.'
|
|
8727
|
+
command: { type: string, maxLength: 512, description: 'The command pattern inside the rule, for a shell that would rather not re-parse `rule`.' }
|
|
8728
|
+
|
|
8729
|
+
RuleRefusalReason:
|
|
8730
|
+
type: string
|
|
8731
|
+
description: >
|
|
8732
|
+
Why a "don't ask again" did NOT get persisted (#154 车二; the server's `RuleRefusalReason`, verbatim).
|
|
8733
|
+
A CLOSED set so a shell can branch instead of matching prose. The two naming styles are NOT a typo:
|
|
8734
|
+
the four hyphenated members are derived from the consent lane's own refusal reasons, the four
|
|
8735
|
+
underscored ones are minted by the server's gates.
|
|
8736
|
+
|
|
8737
|
+
🔴 A refusal NEVER flips the decision. The person's "allow this once" has already taken effect and
|
|
8738
|
+
reached the engine; turning the whole respond into a 4xx would make a real allow vanish. The honest
|
|
8739
|
+
shape is always 200 + `rulePersisted: false` + this reason.
|
|
8740
|
+
|
|
8741
|
+
`no-candidates` — the engine could mint no candidate for this command (compound/redirect/substitution).
|
|
8742
|
+
`unknown-candidate` — the echoed text does not locate in the record's candidate table.
|
|
8743
|
+
`confirm-refused` / `redeem-refused` — the consent protocol's confirm/redeem step was refused
|
|
8744
|
+
(record state, a lost CAS, or the store refusing the write).
|
|
8745
|
+
`rule_lane_unavailable` — FOUR causes share this one value (the server's three conjuncts): no rule store
|
|
8746
|
+
wired, OR the engine minted no candidate for this command, OR the command bytes were unreadable, OR this
|
|
8747
|
+
ask has no verified owner. 🔴 Two of those are per-CARD limits, so this reason is NOT evidence about the
|
|
8748
|
+
deployment — never disable the rule feature globally on the strength of it.
|
|
8749
|
+
`rule_input_edited` — the respond ALSO carried `updatedInput` (ctrl+g edit): the call that takes effect
|
|
8750
|
+
is the edited one while the candidates were minted from the ORIGINAL command, so persisting would arm a
|
|
8751
|
+
standing allow for the original (a real privilege widening). v1 refuses the persistence; the decision
|
|
8752
|
+
itself still stands.
|
|
8753
|
+
`rule_not_offered` — the echoed text is not among THIS ask's candidates (a stale card, or someone
|
|
8754
|
+
trying to mint a rule of their own).
|
|
8755
|
+
`rule_store_error` — the rule store wobbled. The adjudication is unaffected.
|
|
8756
|
+
enum:
|
|
8757
|
+
- no-candidates
|
|
8758
|
+
- unknown-candidate
|
|
8759
|
+
- confirm-refused
|
|
8760
|
+
- redeem-refused
|
|
8761
|
+
- rule_lane_unavailable
|
|
8762
|
+
- rule_input_edited
|
|
8763
|
+
- rule_not_offered
|
|
8764
|
+
- rule_store_error
|
|
8765
|
+
|
|
8275
8766
|
ApprovalRiskAxes:
|
|
8276
8767
|
type: object
|
|
8277
8768
|
description: >
|
|
@@ -9585,7 +10076,10 @@ components:
|
|
|
9585
10076
|
shape, `decision` echoed). Owner-gated with a 404 and NO existence oracle: non-owner and
|
|
9586
10077
|
unknown/settled/expired/wrong-replica ids are indistinguishable.
|
|
9587
10078
|
# 封闭(census 批2 第三段,2026-07-30):铸造点 `tool-approval.ts` 三恒在键 + 两条件键
|
|
9588
|
-
# (rememberApplied/updatedInputForwarded 的条件展开就在同一行字面量里)
|
|
10079
|
+
# (rememberApplied/updatedInputForwarded 的条件展开就在同一行字面量里)。
|
|
10080
|
+
# #154 车二(2026-08-10):`persistRuleAfterDecision` 的 `withFlag()` 在同一个体上再盖两个条件键
|
|
10081
|
+
# (rulePersisted 恒在、ruleRefusal 仅失败时)—— 键集从 5 升 7,**封闭性不动**(additive 不许把
|
|
10082
|
+
# census 闭合的口悄悄捅开)。
|
|
9589
10083
|
required: [approvalId, delivery, decision]
|
|
9590
10084
|
additionalProperties: false
|
|
9591
10085
|
properties:
|
|
@@ -9604,6 +10098,24 @@ components:
|
|
|
9604
10098
|
server 1.241 ([1458]): present (true) when an allow-family decision carried `updatedInput` and
|
|
9605
10099
|
the server forwarded the edited args to the engine — forwarded, not necessarily applied
|
|
9606
10100
|
(consumption depends on the core OnAsk object arm). Omitted on deny / no edit / older servers.
|
|
10101
|
+
rulePersisted:
|
|
10102
|
+
type: boolean
|
|
10103
|
+
description: >
|
|
10104
|
+
server #154 车二, ADDITIVE: whether the `persistRule` this respond carried actually landed in the
|
|
10105
|
+
rule store.
|
|
10106
|
+
|
|
10107
|
+
🔴 A FAILED PERSISTENCE NEVER FLIPS THE DECISION — the person's "allow this once" already took
|
|
10108
|
+
effect and reached the engine, so the honest shape is 200 + `rulePersisted: false` + `ruleRefusal`,
|
|
10109
|
+
NOT a 4xx. Render it as "allowed, but 'don't ask again' was not saved" — never as a failed approval.
|
|
10110
|
+
Omitted when the respond carried no `persistRule`, and on older servers; ABSENT is not `false`.
|
|
10111
|
+
ruleRefusal:
|
|
10112
|
+
allOf: [{ $ref: '#/components/schemas/RuleRefusalReason' }]
|
|
10113
|
+
description: >
|
|
10114
|
+
server #154 车二, ADDITIVE: the MACHINE-READABLE reason when `rulePersisted` is false (closed
|
|
10115
|
+
vocabulary). Always absent when `rulePersisted` is true. Branch on it to separate "try again on the
|
|
10116
|
+
next ask" (`rule_input_edited`) from "there was nothing redeemable on this card"
|
|
10117
|
+
(`rule_lane_unavailable`). 🔴 `rule_lane_unavailable` covers four causes, two of them per-card — do
|
|
10118
|
+
NOT read it as "this deployment has no rule lane" (see the RuleRefusalReason schema).
|
|
9607
10119
|
|
|
9608
10120
|
UsageMetric:
|
|
9609
10121
|
type: string
|
|
@@ -9828,3 +10340,358 @@ components:
|
|
|
9828
10340
|
path: { type: string, description: 'The relPath — pass VERBATIM as ?path= on the file sub-route.' }
|
|
9829
10341
|
hash: { type: string, description: 'Content address (sha256) — enables cross-snapshot change highlighting ([1894]②).' }
|
|
9830
10342
|
size: { type: integer, description: 'Present only when the store has a sizes face (SQL backends).' }
|
|
10343
|
+
|
|
10344
|
+
# ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
10345
|
+
# #154 车二(design/179 §7)—— 持久权限规则的 CC settings 导入面。形按 server 真码 + core 原形亲读
|
|
10346
|
+
# (`src/http/routes/rules.ts` / `src/rules-consent.ts`;preview/result 两形是 core `ImportPreview` /
|
|
10347
|
+
# `ImportResult` 的原样透传)。
|
|
10348
|
+
# ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
10349
|
+
|
|
10350
|
+
CcImportSettingsLayer:
|
|
10351
|
+
type: string
|
|
10352
|
+
enum: [userSettings, projectSettings, localSettings]
|
|
10353
|
+
description: >
|
|
10354
|
+
The three user-editable CC settings layers this version reads (core `ImportedSettingsLayer`). The two
|
|
10355
|
+
layers it does NOT read are named in `CcImportPreview.uncovered` rather than being silently absent.
|
|
10356
|
+
|
|
10357
|
+
RuleScope:
|
|
10358
|
+
description: >
|
|
10359
|
+
Where a persisted rule applies (core `RuleScope`). `project.root` is the directory prefix that decides
|
|
10360
|
+
which cwds the rule is live under.
|
|
10361
|
+
oneOf:
|
|
10362
|
+
- type: object
|
|
10363
|
+
additionalProperties: false
|
|
10364
|
+
required: [kind]
|
|
10365
|
+
properties:
|
|
10366
|
+
kind: { type: string, enum: [global] }
|
|
10367
|
+
- type: object
|
|
10368
|
+
additionalProperties: false
|
|
10369
|
+
required: [kind, root]
|
|
10370
|
+
properties:
|
|
10371
|
+
kind: { type: string, enum: [project] }
|
|
10372
|
+
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.' }
|
|
10373
|
+
|
|
10374
|
+
RuleCandidate:
|
|
10375
|
+
type: object
|
|
10376
|
+
description: 'One candidate rule: the canonical text plus the scope it would land in (core `RuleCandidate`).'
|
|
10377
|
+
additionalProperties: false
|
|
10378
|
+
required: [rule, scope]
|
|
10379
|
+
properties:
|
|
10380
|
+
rule: { type: string, maxLength: 512 }
|
|
10381
|
+
scope: { $ref: '#/components/schemas/RuleScope' }
|
|
10382
|
+
|
|
10383
|
+
CcImportSkippedRule:
|
|
10384
|
+
type: object
|
|
10385
|
+
description: >
|
|
10386
|
+
One entry that did NOT become a candidate.
|
|
10387
|
+
|
|
10388
|
+
🔴 `reason` IS HUMAN-READABLE PROSE, NOT A MACHINE CODE. Upstream produces exactly three shapes:
|
|
10389
|
+
a grammar/validation refusal is `"<RuleRejectCode>: <message>"` (code-PREFIXED prose, e.g.
|
|
10390
|
+
`"invalid.bare_interpreter_prefix: …"` — never the bare code); a layer whose JSON does not parse is
|
|
10391
|
+
`"settings file is not valid JSON (<error text>)"`; a non-string allow entry is
|
|
10392
|
+
`"settings entry is not a string"`. If you must classify, key on the prefix BEFORE the first `:` and
|
|
10393
|
+
handle the two unprefixed shapes — do not equality-match the whole string and do not expect a bare code.
|
|
10394
|
+
|
|
10395
|
+
⚠️ `rule` is not always a rule text either: on the unparseable-JSON arm it carries THAT LAYER'S PATH
|
|
10396
|
+
(no entry was parsed, so there is no rule to name). Do not assume a `Bash(...)` shape when rendering.
|
|
10397
|
+
additionalProperties: false
|
|
10398
|
+
required: [rule, reason]
|
|
10399
|
+
properties:
|
|
10400
|
+
rule: { type: string }
|
|
10401
|
+
reason: { type: string }
|
|
10402
|
+
|
|
10403
|
+
CcImportLayerReport:
|
|
10404
|
+
type: object
|
|
10405
|
+
description: >
|
|
10406
|
+
Per-layer read report inside a preview.
|
|
10407
|
+
|
|
10408
|
+
⚠️ ON THIS HTTP ROUTE `found` IS ALWAYS TRUE. Upstream only writes false when its injected file reader
|
|
10409
|
+
returns undefined or throws, and the server adapter hands back the request's own REQUIRED `content`
|
|
10410
|
+
field — so it can never be absent. Empty text, `{}`, and a file with no allow bucket all still report
|
|
10411
|
+
`found: true` (they simply yield no candidates). Do NOT use `found` as "did this layer contribute
|
|
10412
|
+
rules" — read `candidates` for that. The field is kept because it is part of core's preview shape
|
|
10413
|
+
(it only discriminates for a host whose reader touches a real filesystem).
|
|
10414
|
+
additionalProperties: false
|
|
10415
|
+
required: [path, layer, found]
|
|
10416
|
+
properties:
|
|
10417
|
+
path: { type: string }
|
|
10418
|
+
layer: { $ref: '#/components/schemas/CcImportSettingsLayer' }
|
|
10419
|
+
found: { type: boolean }
|
|
10420
|
+
|
|
10421
|
+
CcImportLayer:
|
|
10422
|
+
type: object
|
|
10423
|
+
description: >
|
|
10424
|
+
One settings layer submitted to rulesCcImportPrepare. The CONTENT is submitted by the caller because a
|
|
10425
|
+
cloud deployment has no access to the user's filesystem — that is the deployment shape, not a bypass:
|
|
10426
|
+
what is read is still only the allow bucket.
|
|
10427
|
+
|
|
10428
|
+
🔴 `root` MUST be non-empty (an empty prefix contains every cwd — see RuleScope). Its TRUTHFULNESS is
|
|
10429
|
+
deliberately NOT validated: the server has no such tree to check against, and pretending to validate
|
|
10430
|
+
would be a lie. It is the user's own label for "which cwds should this rule be live under".
|
|
10431
|
+
# 封闭:server 侧 zod 是 `.strict()`(把 `content` 拼成 `contents` 的客户端应当场 400,而不是拿到
|
|
10432
|
+
# 一份「零候选」的预览去纳闷)。
|
|
10433
|
+
additionalProperties: false
|
|
10434
|
+
required: [layer, path, root, content]
|
|
10435
|
+
properties:
|
|
10436
|
+
layer: { $ref: '#/components/schemas/CcImportSettingsLayer' }
|
|
10437
|
+
path: { type: string, minLength: 1, maxLength: 1024 }
|
|
10438
|
+
root: { type: string, minLength: 1, maxLength: 1024 }
|
|
10439
|
+
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).' }
|
|
10440
|
+
|
|
10441
|
+
CcImportPreview:
|
|
10442
|
+
type: object
|
|
10443
|
+
description: >
|
|
10444
|
+
What the import WOULD do, shown before anyone confirms (core `ImportPreview`).
|
|
10445
|
+
|
|
10446
|
+
`uncovered` is a TWO-KEY RECORD, not a list: the two layers this version does not read are named in the
|
|
10447
|
+
TYPE, so an empty list, a missing member or a duplicate is not expressible. A preview that reported only
|
|
10448
|
+
the layers it read would be claiming a completeness it does not have.
|
|
10449
|
+
additionalProperties: false
|
|
10450
|
+
required: [candidates, skipped, layers, uncovered]
|
|
10451
|
+
properties:
|
|
10452
|
+
candidates:
|
|
10453
|
+
type: array
|
|
10454
|
+
items: { $ref: '#/components/schemas/RuleCandidate' }
|
|
10455
|
+
description: 'What would be persisted, in redemption order.'
|
|
10456
|
+
skipped:
|
|
10457
|
+
type: array
|
|
10458
|
+
items: { $ref: '#/components/schemas/CcImportSkippedRule' }
|
|
10459
|
+
description: 'Entries that did not become candidates. `reason` is PROSE, not a code — see the schema.'
|
|
10460
|
+
layers:
|
|
10461
|
+
type: array
|
|
10462
|
+
items: { $ref: '#/components/schemas/CcImportLayerReport' }
|
|
10463
|
+
uncovered:
|
|
10464
|
+
type: object
|
|
10465
|
+
additionalProperties: false
|
|
10466
|
+
required: [flagSettings, policySettings]
|
|
10467
|
+
properties:
|
|
10468
|
+
flagSettings: { type: string, enum: [not-imported-v1] }
|
|
10469
|
+
policySettings: { type: string, enum: [not-imported-v1] }
|
|
10470
|
+
|
|
10471
|
+
CcImportPrepareResult:
|
|
10472
|
+
type: object
|
|
10473
|
+
description: >
|
|
10474
|
+
The 200 body of rulesCcImportPrepare. NO RULE is in the store yet — the writer is only reached by
|
|
10475
|
+
rulesCcImportRedeem. (A pending approval record and this ticket ARE durable already; see the endpoint
|
|
10476
|
+
description — prepare is not idempotent.)
|
|
10477
|
+
additionalProperties: false
|
|
10478
|
+
required: [preview, ticket, expiresAtMs]
|
|
10479
|
+
properties:
|
|
10480
|
+
preview: { $ref: '#/components/schemas/CcImportPreview' }
|
|
10481
|
+
ticket:
|
|
10482
|
+
type: string
|
|
10483
|
+
description: >
|
|
10484
|
+
The redemption ticket: principal-bound, expiring, atomically single-use, and payload-bound (it pins
|
|
10485
|
+
a digest of the candidate set at mint time, so tampering with the record and redeeming an old ticket
|
|
10486
|
+
fails an equality rather than relying on "only we can write the record"). NOT a cacheable credential;
|
|
10487
|
+
never hand it to another user.
|
|
10488
|
+
expiresAtMs: { type: integer, description: 'Absolute server-clock expiry (the window is 10 minutes — one interaction, not a session).' }
|
|
10489
|
+
|
|
10490
|
+
CcImportResult:
|
|
10491
|
+
type: object
|
|
10492
|
+
description: >
|
|
10493
|
+
What the import ACTUALLY did (core `ImportResult`) — a different moment and a different contract from
|
|
10494
|
+
the preview, because dedup, concurrency and redemption-time validation can each move an entry between
|
|
10495
|
+
the two.
|
|
10496
|
+
|
|
10497
|
+
⚠️ `skippedAtRedeem` is empty on every success arm of THIS lane: the server treats a non-empty one as
|
|
10498
|
+
INDETERMINATE, releases the claim and answers 503 `state.rule_import_retry` instead of returning a 200
|
|
10499
|
+
that quietly means "half of it did not land". The field stays on the shape because it is core's.
|
|
10500
|
+
additionalProperties: false
|
|
10501
|
+
required: [persisted, deduped, skippedAtRedeem, rev]
|
|
10502
|
+
properties:
|
|
10503
|
+
persisted:
|
|
10504
|
+
type: array
|
|
10505
|
+
items: { $ref: '#/components/schemas/RuleCandidate' }
|
|
10506
|
+
deduped:
|
|
10507
|
+
type: array
|
|
10508
|
+
items: { $ref: '#/components/schemas/RuleCandidate' }
|
|
10509
|
+
description: 'Already in the store — not a failure.'
|
|
10510
|
+
skippedAtRedeem:
|
|
10511
|
+
type: array
|
|
10512
|
+
items: { $ref: '#/components/schemas/CcImportSkippedRule' }
|
|
10513
|
+
rev: { type: integer, description: 'The rule store''s monotonic revision after the batch.' }
|
|
10514
|
+
|
|
10515
|
+
CcImportRedeemResult:
|
|
10516
|
+
type: object
|
|
10517
|
+
description: 'The 200 body of rulesCcImportRedeem.'
|
|
10518
|
+
additionalProperties: false
|
|
10519
|
+
required: [result]
|
|
10520
|
+
properties:
|
|
10521
|
+
result: { $ref: '#/components/schemas/CcImportResult' }
|
|
10522
|
+
|
|
10523
|
+
# ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
10524
|
+
# design/183 —— 身份收编(form b:纯身份重绑)。形逐字段照 server `src/adoption/wire.ts` 的 zod
|
|
10525
|
+
# (那份 schema 自陈是「跨仓 wire 契约,下游按字段名逐字钉围栏」)。
|
|
10526
|
+
# ═══════════════════════════════════════════════════════════════════════════════════════════════
|
|
10527
|
+
|
|
10528
|
+
AdoptionRequest:
|
|
10529
|
+
type: object
|
|
10530
|
+
description: 'The POST /v1/adoption body.'
|
|
10531
|
+
additionalProperties: false # server 侧 `.strict()`:多余键是调用方错误,当场 400(不静默吞)
|
|
10532
|
+
required: [fromPrincipal, toPrincipal]
|
|
10533
|
+
properties:
|
|
10534
|
+
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).' }
|
|
10535
|
+
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.' }
|
|
10536
|
+
|
|
10537
|
+
AdoptionSource:
|
|
10538
|
+
type: object
|
|
10539
|
+
description: 'Where the adoption came from. Form b has exactly one shape — a named principal.'
|
|
10540
|
+
additionalProperties: false
|
|
10541
|
+
required: [kind, principal]
|
|
10542
|
+
properties:
|
|
10543
|
+
kind: { type: string, enum: [principal] }
|
|
10544
|
+
principal: { type: string, minLength: 1 }
|
|
10545
|
+
|
|
10546
|
+
AdoptionLegAction:
|
|
10547
|
+
type: string
|
|
10548
|
+
enum: [bucket-rebind, row-rewrite, carried, none, reset]
|
|
10549
|
+
description: 'The per-store migration verb (closed set; form b only ever emits the first two plus `none`).'
|
|
10550
|
+
|
|
10551
|
+
AdoptionLeg:
|
|
10552
|
+
type: object
|
|
10553
|
+
description: 'One migration leg''s receipt.'
|
|
10554
|
+
additionalProperties: false
|
|
10555
|
+
required: [store, action]
|
|
10556
|
+
properties:
|
|
10557
|
+
store: { type: string, minLength: 1, description: 'Stable leg id = `<table>#<identity column>` (the server schema''s real names — reconcile against it).' }
|
|
10558
|
+
action: { $ref: '#/components/schemas/AdoptionLegAction' }
|
|
10559
|
+
rows:
|
|
10560
|
+
type: integer
|
|
10561
|
+
minimum: 0
|
|
10562
|
+
description: >
|
|
10563
|
+
Rows this leg changed when it RAN. 🔴 NOT a per-request delta and NOT a late-sweep delta: legs are
|
|
10564
|
+
only ever exposed inside `immutableReport`, so on a terminal arc every re-run replays the FROZEN
|
|
10565
|
+
first-run counts (a non-zero value here says nothing about what the current request did). On a
|
|
10566
|
+
`stalled` receipt it is the current row's reading, which can still move.
|
|
10567
|
+
quarantined: { type: integer, minimum: 0 }
|
|
10568
|
+
|
|
10569
|
+
AdoptionConfigEntry:
|
|
10570
|
+
type: object
|
|
10571
|
+
description: >
|
|
10572
|
+
One affected DEPLOYMENT configuration. 🔴 `migrated` is always false on the server half: these live in
|
|
10573
|
+
OTHER deployment units (BFF / cli / env / core declarations) that the adoption cannot reach — and a
|
|
10574
|
+
configuration one cannot change must not be reported as changed. The list being present, plus
|
|
10575
|
+
`migrated:false`, plus an unsettled account, is together the evidence for "configuration does not
|
|
10576
|
+
migrate, stated honestly".
|
|
10577
|
+
additionalProperties: false
|
|
10578
|
+
required: [deployment, key, requiredValue, migrated, ack, witnessedAtMs]
|
|
10579
|
+
properties:
|
|
10580
|
+
deployment: { type: string, minLength: 1 }
|
|
10581
|
+
key: { type: string, minLength: 1 }
|
|
10582
|
+
requiredValue: { type: string }
|
|
10583
|
+
migrated: { type: boolean }
|
|
10584
|
+
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).' }
|
|
10585
|
+
witnessedAtMs:
|
|
10586
|
+
type: [integer, "null"]
|
|
10587
|
+
description: 'When a consumer''s runtime typed receipt actually read `requiredValue` back. 🔴 `null` = not witnessed = NOT settled.'
|
|
10588
|
+
|
|
10589
|
+
AdoptionNotMigratedFace:
|
|
10590
|
+
type: string
|
|
10591
|
+
enum: [usage-window, cost-quota, rate-limit, approval-exemption-grantor, approval-ask-decision-actor, image-bake-requested-by]
|
|
10592
|
+
description: >
|
|
10593
|
+
FROZEN machine-readable vocabulary of the faces the server deliberately does not migrate. Renaming or
|
|
10594
|
+
removing a member is a wire break; adding one is a new behaviour face. Consumers assert POSITIVELY on
|
|
10595
|
+
these so that "not migrated by design" and "forgotten" stay distinguishable in a black box.
|
|
10596
|
+
|
|
10597
|
+
AdoptionNotMigrated:
|
|
10598
|
+
type: object
|
|
10599
|
+
description: 'One positive "by design, not migrated" declaration.'
|
|
10600
|
+
additionalProperties: false
|
|
10601
|
+
required: [face, ruling, reason]
|
|
10602
|
+
properties:
|
|
10603
|
+
face: { $ref: '#/components/schemas/AdoptionNotMigratedFace' }
|
|
10604
|
+
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.' }
|
|
10605
|
+
reason: { type: string, minLength: 1 }
|
|
10606
|
+
|
|
10607
|
+
AdoptionReport:
|
|
10608
|
+
type: object
|
|
10609
|
+
description: >
|
|
10610
|
+
The adoption report.
|
|
10611
|
+
|
|
10612
|
+
🔴 "IMMUTABLE" HOLDS ONLY ONCE `status` IS `adopted`. The frozen row is written once at the final phase
|
|
10613
|
+
and replayed byte-for-byte thereafter, never recomputed — that byte-identity IS the idempotency
|
|
10614
|
+
criterion. But a `stalled` receipt has no such row yet, so the server PROJECTS a same-shaped report
|
|
10615
|
+
from the CURRENT row (`atMs` = the row's updated-at, configs/legs = the current columns). That
|
|
10616
|
+
projection is provisional: the next read can differ in every field. Do NOT persist a `stalled`
|
|
10617
|
+
report as audit evidence — take the frozen one after `status === "adopted"`. The two share one shape
|
|
10618
|
+
by upstream ruling (there is only one envelope); the discriminator is `status`, not the field name.
|
|
10619
|
+
additionalProperties: false
|
|
10620
|
+
required: [adoptionId, form, from, toPrincipal, atMs, affectedDeploymentConfigs, legs, notMigratedByDesign]
|
|
10621
|
+
properties:
|
|
10622
|
+
adoptionId: { type: string, minLength: 1 }
|
|
10623
|
+
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.' }
|
|
10624
|
+
from: { $ref: '#/components/schemas/AdoptionSource' }
|
|
10625
|
+
toPrincipal: { type: string, minLength: 1 }
|
|
10626
|
+
atMs: { type: integer, minimum: 0 }
|
|
10627
|
+
affectedDeploymentConfigs:
|
|
10628
|
+
type: array
|
|
10629
|
+
items: { $ref: '#/components/schemas/AdoptionConfigEntry' }
|
|
10630
|
+
legs:
|
|
10631
|
+
type: array
|
|
10632
|
+
items: { $ref: '#/components/schemas/AdoptionLeg' }
|
|
10633
|
+
notMigratedByDesign:
|
|
10634
|
+
type: array
|
|
10635
|
+
items: { $ref: '#/components/schemas/AdoptionNotMigrated' }
|
|
10636
|
+
|
|
10637
|
+
AdoptionCurrent:
|
|
10638
|
+
type: object
|
|
10639
|
+
description: 'Recomputed on every read (the counterpart to AdoptionReport''s frozen history — do not infer one from the other).'
|
|
10640
|
+
additionalProperties: false
|
|
10641
|
+
required: [outstandingConfigs, quarantined, residualSourceRows]
|
|
10642
|
+
properties:
|
|
10643
|
+
outstandingConfigs:
|
|
10644
|
+
type: array
|
|
10645
|
+
items: { type: string }
|
|
10646
|
+
description: 'Config entries whose `witnessedAtMs` is null, as `<deployment>:<key>`. Empty = the account is settled.'
|
|
10647
|
+
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.' }
|
|
10648
|
+
residualSourceRows:
|
|
10649
|
+
type: integer
|
|
10650
|
+
minimum: 0
|
|
10651
|
+
description: >
|
|
10652
|
+
Rows STILL under the old identity, recomputed on every read. AGGREGATION: counted PER TABLE with
|
|
10653
|
+
the table's identity axes unioned, so a row carrying the old identity on two or three axes counts
|
|
10654
|
+
ONCE; the per-table counts are then summed. It is deliberately NOT a sum over legs — reconciling
|
|
10655
|
+
this number against per-leg `rows` will not add up.
|
|
10656
|
+
|
|
10657
|
+
🔴 Load-bearing: adoption does NOT fence ordinary writers ("stop the engine first" is an operator
|
|
10658
|
+
precondition, not a machine guarantee), so a caller still injecting the old identity keeps writing
|
|
10659
|
+
rows after the terminal state while the idempotent short-circuit keeps answering `adopted`. So `> 0`
|
|
10660
|
+
means "rows may still sit under the old identity, OR the count could not be taken" — either way, GO
|
|
10661
|
+
LOOK. It is NOT proof that the deployment is actively writing. A POST also sweeps those late rows in
|
|
10662
|
+
and re-reports this; a GET does not sweep (though it does resume an in-flight arc — see the GET
|
|
10663
|
+
description).
|
|
10664
|
+
|
|
10665
|
+
⚠️ `1` IS AMBIGUOUS. When the count itself fails (query error / parse failure) the server reports
|
|
10666
|
+
`1` fail-closed rather than `0`, because `0` is the literal assertion "everything is clean" and it
|
|
10667
|
+
has not earned the right to say that. So `1` may mean "one residual row" OR "could not measure";
|
|
10668
|
+
the wire cannot tell them apart (the server log carries `adoption_residual_count_failed`). Use it
|
|
10669
|
+
as intended — `> 0` ⇒ GO LOOK — but never feed `=== 1` to a report as the exact number of rows.
|
|
10670
|
+
|
|
10671
|
+
AdoptionReceipt:
|
|
10672
|
+
type: object
|
|
10673
|
+
description: >
|
|
10674
|
+
The ONE adoption receipt envelope — the same SHAPE for the first success and for every idempotent
|
|
10675
|
+
re-run. No second receipt shape exists anywhere.
|
|
10676
|
+
|
|
10677
|
+
⚠️ Same shape carries NO byte-identity guarantee: `current` is recomputed on every call (residual
|
|
10678
|
+
count, config ledger) so two reads may match or may differ, and before the terminal state
|
|
10679
|
+
`immutableReport` is itself a provisional projection. The one thing guaranteed frozen is a TERMINAL
|
|
10680
|
+
`immutableReport`.
|
|
10681
|
+
|
|
10682
|
+
🔴 `adoptionId` is at the TOP LEVEL (the server sends `{adoptionId, ...receipt}`), carrying the same
|
|
10683
|
+
value as `immutableReport.adoptionId`.
|
|
10684
|
+
🔴 `status` is a TWO-WORD closed set. `rejected` is deliberately NOT a member: a refused adoption never
|
|
10685
|
+
produces a receipt, it answers a typed 409 — otherwise "refused" and "succeeded" would share one 200
|
|
10686
|
+
envelope and a consumer could only guess by reading fields.
|
|
10687
|
+
🔴 `status` is ALSO the precondition for reading `immutableReport`: it is frozen only once `adopted`;
|
|
10688
|
+
on `stalled` it is a provisional projection of the current row (see AdoptionReport).
|
|
10689
|
+
additionalProperties: false
|
|
10690
|
+
required: [adoptionId, status, immutableReport, current]
|
|
10691
|
+
properties:
|
|
10692
|
+
adoptionId: { type: string, minLength: 1 }
|
|
10693
|
+
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.' }
|
|
10694
|
+
immutableReport:
|
|
10695
|
+
allOf: [{ $ref: '#/components/schemas/AdoptionReport' }]
|
|
10696
|
+
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.'
|
|
10697
|
+
current: { $ref: '#/components/schemas/AdoptionCurrent' }
|