@sema-agent/sdk 17.1.0 → 18.0.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.
Files changed (46) hide show
  1. package/README.md +143 -0
  2. package/dist/client.d.ts +3 -0
  3. package/dist/client.d.ts.map +1 -1
  4. package/dist/client.js +4 -0
  5. package/dist/client.js.map +1 -1
  6. package/dist/device/index.d.ts +14 -0
  7. package/dist/device/index.d.ts.map +1 -1
  8. package/dist/device/index.js.map +1 -1
  9. package/dist/errors.d.ts +11 -0
  10. package/dist/errors.d.ts.map +1 -1
  11. package/dist/errors.js +11 -0
  12. package/dist/errors.js.map +1 -1
  13. package/dist/events.d.ts +15 -0
  14. package/dist/events.d.ts.map +1 -1
  15. package/dist/events.js.map +1 -1
  16. package/dist/health.d.ts +95 -31
  17. package/dist/health.d.ts.map +1 -1
  18. package/dist/health.js.map +1 -1
  19. package/dist/index.d.ts +2 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +1 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/resources/admin.d.ts +65 -0
  24. package/dist/resources/admin.d.ts.map +1 -0
  25. package/dist/resources/admin.js +27 -0
  26. package/dist/resources/admin.js.map +1 -0
  27. package/dist/resources/devices.d.ts +2 -1
  28. package/dist/resources/devices.d.ts.map +1 -1
  29. package/dist/resources/devices.js +2 -1
  30. package/dist/resources/devices.js.map +1 -1
  31. package/dist/resources/ops.d.ts +2 -1
  32. package/dist/resources/ops.d.ts.map +1 -1
  33. package/dist/resources/ops.js +2 -1
  34. package/dist/resources/ops.js.map +1 -1
  35. package/dist/resources/rules.d.ts +26 -9
  36. package/dist/resources/rules.d.ts.map +1 -1
  37. package/dist/resources/rules.js.map +1 -1
  38. package/dist/resources/sessions.d.ts +2 -1
  39. package/dist/resources/sessions.d.ts.map +1 -1
  40. package/dist/resources/sessions.js +2 -1
  41. package/dist/resources/sessions.js.map +1 -1
  42. package/dist/types.d.ts +35 -1
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/openapi.yaml +178 -17
  46. package/package.json +2 -2
package/openapi.yaml CHANGED
@@ -184,6 +184,10 @@ tags:
184
184
  description: v2 leader pipeline (gated; E2B + git push). NOT the default door-B brain.
185
185
  - name: metrics
186
186
  description: Worker health/cost (ops; uses metricsToken, not a principal).
187
+ - name: admin
188
+ description: >
189
+ Operator-only `/v1/admin/*` actions on THIS engine instance. Today: the drain endpoint (server >= 7.121.0 contract
190
+ body `DrainRequest` / receipt `DrainReceipt`, owned by `@sema-agent/settings-schema` `./fleet`).
187
191
 
188
192
  paths:
189
193
 
@@ -5595,6 +5599,65 @@ paths:
5595
5599
  '403': { description: 'Multi-tenant worker: wiring diagnostics are operator-only (`auth.operator_only`). With an EMPTY `OPERATOR_PRINCIPALS` the answer is instead 501 `capability.operator_principals_required` (server >= 7.116.0; any identity, ahead of authorization — branch on `errorCode`, not the status).' }
5596
5600
  '404': { description: 'This worker serves no wiring diagnostics (older server, or a process not assembled by the composition root).' }
5597
5601
 
5602
+ /v1/admin/drain:
5603
+ parameters:
5604
+ - $ref: '#/components/parameters/PrincipalHeader'
5605
+ post:
5606
+ tags: [admin]
5607
+ operationId: adminDrain
5608
+ x-status: live # server >= 7.121.0 contract body; 7.35.0–7.120.x took a retired `{reason}` body.
5609
+ summary: Put THIS engine instance into draining by a deadline (operator-only).
5610
+ description: >
5611
+ Server >= 7.121.0. The body is the contract `DrainRequest` (owned by `@sema-agent/settings-schema` >= 9.0.0 `./fleet`), the answer
5612
+ the contract `DrainReceipt`. Entering drain: new work is refused (billable submits 503 `draining`; HITL answers — including the
5613
+ durable `/decide` — and resumes are ADMITTED), `GET /readyz` turns 503, preemptible in-flight legs park durably at their next clean
5614
+ turn boundary, live approval cards are re-routed to durable parks; the engine stops itself when in-flight reaches zero or at
5615
+ `deadlineAt` (fixed when entered — a later hot change to the drain window does not move it). After a drain entered through this
5616
+ endpoint, the FIRST stop signal the platform sends is absorbed (deadline unchanged) and only a later one, at least the minimum gap
5617
+ apart, hard-stops — so the platform stop timeout (Docker `stop_grace_period`, systemd `TimeoutStopSec`, k8s
5618
+ `terminationGracePeriodSeconds`) must be >= the requested deadline.
5619
+ IDENTITY — the request names WHICH instance, compared field by field (no state change on a mismatch, 409 `conflict.fleet_identity`
5620
+ naming the field, never echoing the value): `worker` = the engine's worker name (`SEMA_REGISTRY_WORKER`; an engine without a center
5621
+ lane has none, so every request mismatches); `expectedGeneration` = the fleet generation (`/health.generation`), ABSENT AGAINST ABSENT
5622
+ (omitting it is the standalone form, not a wildcard — it mismatches a fleet member); `scope` = the tenant scope, both sides read as a
5623
+ value (the engine's = env `FLEET_SCOPE`; unset on the engine and omitted in the request both read as the default scope `global`);
5624
+ `instanceId` (`/health.instanceId`) is compared only when present.
5625
+ IDEMPOTENT — the same `actionId` (or any other action) while already draining answers 200 `already_draining` and echoes the FIRST
5626
+ drain's `since` / `deadlineAt`, changing nothing. Clients do not auto-retry this POST; re-send with the same `actionId` to retry.
5627
+ RETIRED (zero alias): the 7.35.0–7.120.x `{reason}` "shutdown reason declaration" body (it only recorded a sentence and never
5628
+ drained) now answers 400 `request.field_invalid`; `/health.drainReason` and the `reason` key on 503 `draining` bodies are gone too.
5629
+ Gate order (same as every operator face): identity -> authorization -> body shape -> drain entry -> four-part identity.
5630
+ requestBody:
5631
+ required: true
5632
+ content:
5633
+ application/json:
5634
+ schema: { $ref: '#/components/schemas/DrainRequest' }
5635
+ responses:
5636
+ '200':
5637
+ description: Draining (this call entered it) or already draining (the first drain's facts, unchanged).
5638
+ content:
5639
+ application/json:
5640
+ schema: { $ref: '#/components/schemas/DrainReceipt' }
5641
+ '400': { $ref: '#/components/responses/BadRequest' } # request.field_invalid — the body is not a DrainRequest (including the retired `{reason}` form); nothing was changed
5642
+ '401': { $ref: '#/components/responses/Unauthorized' } # auth.unauthorized (credential gate) / auth.principal_required (REQUIRE_PRINCIPAL deployment, no usable identity)
5643
+ '403': { description: '`auth.operator_only` — OPERATOR_PRINCIPALS is non-empty and the caller is not in it.' }
5644
+ '409':
5645
+ description: >-
5646
+ `conflict.fleet_identity` (server >= 7.121.0) — the request's four-part identity (`scope` / `worker` / `expectedGeneration` /
5647
+ `instanceId`) does not match this engine; the sentence names the mismatched fields (never the values). Nothing was changed.
5648
+ Compare with the target engine's `GET /health` (`instanceId` / `generation`), its worker name and `FLEET_SCOPE`, then re-send.
5649
+ content:
5650
+ application/json:
5651
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
5652
+ '501':
5653
+ description: >-
5654
+ `capability.operator_principals_required` — OPERATOR_PRINCIPALS is empty, so no identity is an operator on this deployment (any
5655
+ deployment shape, ahead of authorization). `capability.drain_state_required` — the host has no shutdown chain wired (an embedded
5656
+ engine), so there is no drain to enter (answered after the body check). Branch on `errorCode`.
5657
+ content:
5658
+ application/json:
5659
+ schema: { $ref: '#/components/schemas/ErrorResponse' }
5660
+
5598
5661
  /v1/sendfile-links:
5599
5662
  parameters:
5600
5663
  - $ref: '#/components/parameters/PrincipalHeader'
@@ -5804,7 +5867,7 @@ paths:
5804
5867
  content:
5805
5868
  application/json:
5806
5869
  schema: { $ref: '#/components/schemas/CcImportPrepareResult' }
5807
- '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)
5870
+ '400': { $ref: '#/components/responses/BadRequest' } # request.body_shape — not {layers:[{layer,path,root,content}]}, or a layer `root` the server does not accept (server >= 7.120.0: not a complete absolute path — see RuleScope; sentence "layers[<i>].root must be an absolute path" on >= 7.121.0, the scope sentence on 7.120.x; no ticket minted; <= 7.119.x: an empty `root`, answered with the body-shape sentence)
5808
5871
  '401': { $ref: '#/components/responses/Unauthorized' }
5809
5872
  '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)
5810
5873
  '429': { $ref: '#/components/responses/RateLimited' }
@@ -5931,8 +5994,8 @@ paths:
5931
5994
  required: false
5932
5995
  schema: { type: string, minLength: 1 }
5933
5996
  description: >
5934
- Filter to one scope, spelled `global` or `project:<root>` with a NON-EMPTY root. Unreadable ⇒ 400
5935
- `request.query_invalid` (never silently unfiltered). Matching is done on the CANONICAL form, so an
5997
+ Filter to one scope, spelled `global` or `project:<root>` (which roots are accepted: see RuleScope). Unreadable
5998
+ or a root the server does not accept ⇒ 400 `request.query_invalid` (never silently unfiltered). Matching is done on the CANONICAL form, so an
5936
5999
  equivalent-but-differently-spelled root filters to an empty page.
5937
6000
  - name: limit
5938
6001
  in: query
@@ -6015,7 +6078,7 @@ paths:
6015
6078
  content:
6016
6079
  application/json:
6017
6080
  schema: { $ref: '#/components/schemas/RuleWriteResult' }
6018
- '400': { $ref: '#/components/responses/BadRequest' } # request.field_invalid — `behavior: "allow"` (whole-sentence refusal, NO `reason` key), or a content test refused it (closed `reason` + human `detail`: rule_rejected / scope_bidi_controls / scope_control_chars / row_inconsistent / …, the SAME table the local-import preview answers with). request.body_shape — body is not {behavior, rule, scope, principal?}, or `scope` is not `global`/`project:<non-empty root>` (same sentence as DELETE)
6081
+ '400': { $ref: '#/components/responses/BadRequest' } # request.field_invalid — `behavior: "allow"` (whole-sentence refusal, NO `reason` key), or a content test refused it (closed `reason` + human `detail`: rule_rejected / scope_bidi_controls / scope_control_chars / row_inconsistent / …, the SAME table the local-import preview answers with). request.body_shape — body is not {behavior, rule, scope, principal?}, or `scope` is not `global`/`project:<root>` with a root the server accepts (see RuleScope; same sentence as DELETE)
6019
6082
  '401': { $ref: '#/components/responses/Unauthorized' }
6020
6083
  '403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only — writing another principal's rules without explicit OPERATOR_PRINCIPALS membership
6021
6084
  '429': { $ref: '#/components/responses/RateLimited' }
@@ -6085,7 +6148,7 @@ paths:
6085
6148
  content:
6086
6149
  application/json:
6087
6150
  schema: { $ref: '#/components/schemas/RuleRevokeResult' }
6088
- '400': { $ref: '#/components/responses/BadRequest' } # request.body_shape — body is not {behavior, rule, scope, principal?} (server >= 7.67.0: `behavior` is REQUIRED and has no default), or `scope` is not `global`/`project:<non-empty root>`
6151
+ '400': { $ref: '#/components/responses/BadRequest' } # request.body_shape — body is not {behavior, rule, scope, principal?} (server >= 7.67.0: `behavior` is REQUIRED and has no default), or `scope` is not `global`/`project:<root>` with a root the server accepts (see RuleScope)
6089
6152
  '401': { $ref: '#/components/responses/Unauthorized' }
6090
6153
  '403': { $ref: '#/components/responses/Forbidden' } # auth.operator_only — revoking another principal's rule without explicit OPERATOR_PRINCIPALS membership
6091
6154
  '429': { $ref: '#/components/responses/RateLimited' }
@@ -6339,7 +6402,7 @@ paths:
6339
6402
  content:
6340
6403
  application/json:
6341
6404
  schema: { $ref: '#/components/schemas/DeviceEnrollResult' }
6342
- '400': { $ref: '#/components/responses/BadRequest' } # request.body_shape (unknown key / missing required); request.field_invalid (platformOs outside the v1 closed set `darwin|linux` — a third value is never silently accepted; a field over its byte column — a silently truncated value is a DIFFERENT value)
6405
+ '400': { $ref: '#/components/responses/BadRequest' } # request.body_shape (unknown key / missing required); request.field_invalid (platformOs outside the v1 closed set `darwin|linux` — a third value is never silently accepted; a field over its byte column — a silently truncated value is a DIFFERENT value; server >= 7.120.0: `workspaceRoot` not an absolute POSIX path — the token is not consumed)
6343
6406
  '401': { $ref: '#/components/responses/Unauthorized' }
6344
6407
  '403':
6345
6408
  description: >
@@ -6589,7 +6652,12 @@ components:
6589
6652
  ⚠️ On the billable submit set (every POST that starts a model turn — the server's
6590
6653
  `isBillableSubmitPath`, which is wider than the three task/run endpoints) this
6591
6654
  503 slot is SHARED with producers that are not auth at all, so `errorCode` — not the status — is the
6592
- branch key: `draining` (this replica is shutting down; retry against a replacement -> DrainingError),
6655
+ branch key: `draining` (this replica is shutting down; retry against a replacement -> DrainingError; server >= 7.121.0
6656
+ refuses only NEW work while draining — HITL answers, including the durable `/decide`, and resumes are admitted — and the body
6657
+ carries no `reason` key any more),
6658
+ `state.lease_expired` (server >= 7.121.0: this fleet member's serving lease has expired and the center has been unreachable
6659
+ for the deployment's `FLEET_LEASE_OUTAGE_MAX_MS`; new work only — HITL answers and resumes pass, in-flight runs are untouched;
6660
+ it lifts when the center grants again; retry later or route elsewhere -> ServiceStateError),
6593
6661
  `state.model_roster_pending` (roster not loaded yet; retry -> ServiceStateError), and (server >=7.0.0,
6594
6662
  synchronous `POST /v1/tasks` only) `memory.admission_required` — the org-memory admission directory is
6595
6663
  transiently undecidable, refused fail-closed BEFORE any model call, mapped to MemoryAdmissionError.
@@ -6612,7 +6680,7 @@ components:
6612
6680
  schema: { type: integer }
6613
6681
  description: >
6614
6682
  Seconds to wait before retrying. Presence is per producer, not per status:
6615
- `draining` always sends it (15), `state.model_roster_pending` always sends it (5),
6683
+ `draining` always sends it (15), `state.model_roster_pending` always sends it (5), `state.lease_expired` always sends it (60),
6616
6684
  `memory.admission_required` sends it ONLY when the engine supplied a hint — a steady-state refusal
6617
6685
  (no resolver wired) honestly carries neither this header nor the body's `retryAfterSec`.
6618
6686
  The auth legs (401 bad token / 503 missing token) never send it.
@@ -7250,6 +7318,22 @@ components:
7250
7318
  Pin a SEPARATE (usually cheaper) model for compaction. Validated against the catalog on submit — an
7251
7319
  unknown ref is a 400, not a silent fallback to the main model's price.
7252
7320
  appendSystemPrompt: { type: string, description: Text appended to the assembled system prompt for this task. }
7321
+ outputStyle:
7322
+ type: object
7323
+ required: [name]
7324
+ additionalProperties: false
7325
+ description: >-
7326
+ Server >= 7.121.0 (send only when `capabilities.outputStyle` is true; an older server answers 400 `request.body_shape` for the
7327
+ top-level key): the output style's PER-TURN line, handed verbatim to the engine's seat of the same name. Each attachment cycle the
7328
+ engine states "<name> output style is active. <turnReminder>" in its own marker frame (the engine's default sentence when
7329
+ `turnReminder` is absent), echoed on the stream as `steering_injected` with `source: "output_style"`. One fact, one seat: the
7330
+ style's BODY (the guide text) still goes in `settings.outputStyle`; this key carries only the name and the per-turn line.
7331
+ The value's owner is the engine (exactly these two keys, `name` non-empty and at most 256 characters, `turnReminder` a non-empty
7332
+ string when present); the server does not judge it — a bad value is accepted and refused at prepare, the run ends `failed` with
7333
+ code `config.output_style_invalid` (never silently ignored).
7334
+ properties:
7335
+ name: { type: string, minLength: 1, maxLength: 256 }
7336
+ turnReminder: { type: string, minLength: 1 }
7253
7337
  promptProfile:
7254
7338
  type: string
7255
7339
  enum: [simple, classic]
@@ -9221,6 +9305,8 @@ components:
9221
9305
  bgNotifyFailClosed: { type: boolean }
9222
9306
  steerPriority: { type: string }
9223
9307
  hookFailureNotice: { type: boolean, description: 'server >= 7.48.0 (#281): this binary emits a SECOND `hook_notice` kind, `hook_non_blocking_failure` (the user''s own hook broke: exit != 0 and != 2 / timeout / spawn failure / bad JSON). Absent = an older worker that only emits `hook_decision_unavailable` — branch by `kind`, never crash on an unknown one.' }
9308
+ leaseClient: { type: boolean, description: 'server >= 7.121.0, a version bit (always true, rides with the `fleet` object): this binary carries the serving-lease CLIENT — when the engine is a fleet member (`FLEET_GENERATION`) and the center declares in its heartbeat reply that it answers renewals (`servingLease`), the engine renews and is governed by the lease. Deliberately NOT named `servingLease` (that is the center''s bit): this says the client exists, not that a lease is in force — read `/health.leaseState` for that. Absent = an older worker.' }
9309
+ generationEcho: { type: boolean, description: 'server >= 7.121.0, a version bit (always true, rides with the `fleet` object): this binary echoes env `FLEET_GENERATION` as `generation` on the heartbeat and on `/health` (with `leaseState` / `leaseUntil`), and the drain endpoint `POST /v1/admin/drain` compares the four-part identity including `expectedGeneration`. Absent = an older worker.' }
9224
9310
  workspace:
9225
9311
  description: >
9226
9312
  #3 (server 1.299–7.51): workspace-browse capability family — an OBJECT when the E19 snapshot-store
@@ -9257,6 +9343,13 @@ components:
9257
9343
  effortSelection: { type: boolean, description: "`reasoningEffort` accepted per request." }
9258
9344
  compactionModel: { type: boolean, description: "A separate model may be pinned for compaction." }
9259
9345
  appendSystemPrompt: { type: boolean, description: "Per-request system-prompt append is honoured." }
9346
+ outputStyle:
9347
+ type: boolean
9348
+ description: >-
9349
+ server >= 7.121.0, hardcoded true (a predicate-class bit, same family as `approvalDecidedBy` / `sessionReadRedaction`): the
9350
+ top-level request key TaskRequest.outputStyle (the output style's per-turn line) is accepted and handed verbatim to the engine.
9351
+ Send the key only when this is present; absent = an older server, which answers 400 for it. Not the same thing as
9352
+ `taskSettings.outputStyle` (that bit says `settings.outputStyle`, the style BODY, is accepted) — two seats, two facts.
9260
9353
  toolOutput: { type: boolean, description: "`tool_end` carries the model-facing output." }
9261
9354
  messageIdentity: { type: boolean, description: "Content events carry `eventId` (+ `parentToolCallId` for sub-agents)." }
9262
9355
  forwardSubagentEvents: { type: boolean, description: "Sub-agent events are forwarded onto the parent stream." }
@@ -9362,6 +9455,15 @@ components:
9362
9455
  server >= 7.118.0, hardcoded true (a version bit): the three approval decision doors read `decidedBy` (ApprovalDecidedBy).
9363
9456
  Send it only when this is present; absent = an older server (the durable doors and `plan_review` 400 it, the live
9364
9457
  `respond` drops it silently and answers 200).
9458
+ sessionReadRedaction:
9459
+ type: boolean
9460
+ description: >-
9461
+ server >= 7.120.0, hardcoded true (a predicate-class bit, same family as `approvalDecidedBy`): the session HISTORY read
9462
+ (`GET /v1/sessions/:id`, the `messages` string leaves, every fetch form) passes the same redaction as the live and ledger
9463
+ frames (see SessionRecord). Branch on the bit, not on the server version. Bit absent is NOT `false` and NOT "unredacted":
9464
+ a server <= 7.118.x returns the archive as written; a 7.119.x server already redacts but does not report the bit. A true
9465
+ bit does not mean the read face exists (that is `sessions`; without a session audit face that door answers 501 and this
9466
+ bit is still true), nor that the archive holds no credential (the redaction is read-side only).
9365
9467
  approvalReach:
9366
9468
  type: boolean
9367
9469
  description: >-
@@ -12786,7 +12888,8 @@ components:
12786
12888
  tool results, shell output, compaction summaries) pass the SAME redaction as the live and ledger frames — a credential-shaped span reads
12787
12889
  `«redacted:…»`. Key set, count and segment order are verbatim; a history with no credential-shaped span is byte-identical; the archive itself is
12788
12890
  untouched (the redaction is read-side only), and `?blobCap=` truncation runs AFTER redaction. A caller that rebuilds a model context from this
12789
- read gets the placeholder, not the original string.
12891
+ read gets the placeholder, not the original string. Server >= 7.120.0 reports this with the capability bit `sessionReadRedaction`
12892
+ (a 7.119.x server redacts but does not report it).
12790
12893
  # 🔴 **暂不封闭**(census 轴一裁定,2026-07-30):本体是 core `StoredSession.buildContext()` 的投影
12791
12894
  # (server `audit.ts` auditFromStorage),再叠 server 自己的两条 additive 后处理(windowMessages /
12792
12895
  # truncateMessageBlobs)与路由层追加的 owner —— 键集由「core 投影 × 查询参数」共同决定,不是一个
@@ -15499,6 +15602,17 @@ components:
15499
15602
  was armed but declined/failed (check the store, check authorisation), `false` = there was no lane at all
15500
15603
  (wire one up). The two recovery verbs differ, so a consumer MUST render them apart — NEVER fold them into
15501
15604
  one "nobody approved it".
15605
+ `server.model_unreachable` (server-minted, audience `user`; server >= 7.108.0, `taskId` added in server 7.121.0) — a model call on
15606
+ this leg ended in error and its last request failed AT CONNECT (VPN switched mid-session / the proxy read at boot went stale / the
15607
+ gateway is gone) — not the provider's answer. `{ code, targetHost, proxyInUse?, remedy, sessionId, taskId? }`: `code` one of the
15608
+ connect-failure set (`ECONNREFUSED` / `ENOTFOUND` / `EAI_AGAIN` / `EHOSTUNREACH` / `ENETUNREACH` / `UND_ERR_CONNECT_TIMEOUT`),
15609
+ `targetHost` the model endpoint's `host:port` (a proxy address never rides this frame), `proxyInUse` present only when this hop
15610
+ went through a proxy and only as the variable NAME (`HTTP_PROXY` / `HTTPS_PROXY`), `remedy` a fixed sentence. `taskId`
15611
+ (server >= 7.121.0) = this leg's run resource id (streaming leg: `X-Task-Id` / the `meta` frame's `taskId`; background leg: the
15612
+ submit receipt's `taskId`; resume leg: the resumed run's id) — the same value as that run's terminal and its run-domain codes'
15613
+ `detail.taskId`; absent when the leg has no run resource (a streaming leg on a deployment without a run store) or on an older
15614
+ server. NO `runId` (the model-call layer cannot see the engine's run identity): a resume leg and its original leg share the
15615
+ `taskId`. Once per leg per target; it says "this endpoint is unreachable", not "this run failed".
15502
15616
  `session.records_skipped` (K-24; server >= 7.104.0 / S-612 form B; contract §17.4 / appendix D.3) — 🔴 SERVER-MINTED
15503
15617
  (not in core's register; owner, audience `user` and mint all live in the server). The session's saved log was
15504
15618
  OPENED and some records could not be read and were skipped (file session backend only — both SQL backends refuse a
@@ -19583,10 +19697,54 @@ components:
19583
19697
  The three user-editable CC settings layers this version reads (core `ImportedSettingsLayer`). The two
19584
19698
  layers it does NOT read are named in `CcImportPreview.uncovered` rather than being silently absent.
19585
19699
 
19700
+ DrainRequest:
19701
+ type: object
19702
+ required: [actionId, worker, deadlineAt]
19703
+ description: >-
19704
+ The drain endpoint's request (contract `DrainRequest`, `@sema-agent/settings-schema` >= 9.0.0 `./fleet`): "put this instance into
19705
+ draining by this deadline". See `POST /v1/admin/drain` for how the identity fields are compared.
19706
+ properties:
19707
+ actionId: { type: string, minLength: 1, description: 'Idempotency key: the same id again answers `already_draining` and changes nothing. Hand-issued actions pick their own (e.g. `manual:<uuid>`).' }
19708
+ worker: { type: string, minLength: 1, description: The engine's worker name (`SEMA_REGISTRY_WORKER`). }
19709
+ deadlineAt: { type: integer, minimum: 0, description: Epoch milliseconds by which the engine stops on its own. }
19710
+ scope: { type: string, minLength: 1, description: 'The tenant scope; omitted = the default scope `global` (compared against env `FLEET_SCOPE`, unset = `global`).' }
19711
+ expectedGeneration: { type: string, pattern: '^(0|[1-9][0-9]*)$', description: 'The fleet generation as a canonical decimal string (`/health.generation`); absent against absent — omit it only for a non-member.' }
19712
+ instanceId: { type: string, minLength: 1, description: The engine's per-process identity (`/health.instanceId`); compared only when present. }
19713
+ term: { type: string, minLength: 1, description: The claim term the sending executor holds for this action (center-issued, opaque); absent on a direct or hand-issued call. }
19714
+ DrainReceipt:
19715
+ type: object
19716
+ required: [actionId, since, deadlineAt, state]
19717
+ additionalProperties: false
19718
+ description: >-
19719
+ The drain endpoint's answer (contract `DrainReceipt`): the engine's drain fact plus how this call found it. `state: draining` = this
19720
+ call entered drain; `already_draining` = it already was (including a drain the engine entered on its own), and the receipt reports the
19721
+ FIRST drain's facts.
19722
+ properties:
19723
+ actionId: { type: string, minLength: 1, description: The action that entered this drain. }
19724
+ since: { type: integer, minimum: 0, description: 'Epoch ms the drain began (the same value as `/health.drainingSince`).' }
19725
+ deadlineAt: { type: integer, minimum: 0, description: Epoch ms the engine stops on its own (fixed when the drain was entered). }
19726
+ state: { type: string, enum: [draining, already_draining] }
19586
19727
  RuleScope:
19587
19728
  description: >
19588
19729
  Where a persisted rule applies (core `RuleScope`). `project.root` is the directory prefix that decides
19589
19730
  which cwds the rule is live under.
19731
+
19732
+ 🔴 WHICH ROOTS ARE ACCEPTED — the one statement every other mention in this file points to (the cc-import
19733
+ layer `root`, and the `project:<root>` discriminant string on write / revoke / the list filter):
19734
+ server >= 7.120.0 accepts only a COMPLETE ABSOLUTE PATH — POSIX `/…`, a Windows drive root `X:\…` / `X:/…`,
19735
+ or a UNC root `\\host\…` (the engine's durable-scope predicate; every server entry calls it rather than
19736
+ keeping a copy). A relative root (`proj`, `./x`, `~/x`) or a drive without a root (`C:`, `C:x`) is a 400 — code
19737
+ `request.body_shape` on write / revoke / cc-import prepare, `request.query_invalid` on the list filter. Branch on `errorCode`;
19738
+ the sentence is for people: on write / revoke / the list filter the `error` is exactly "scope must be 'global' or
19739
+ 'project:<root>' with an absolute root"; on cc-import prepare (whose body has no `scope` field) server >= 7.121.0 answers
19740
+ "layers[<i>].root must be an absolute path" (`<i>` = the first refused layer, from 0; same predicate), while 7.120.x
19741
+ answered with the scope sentence.
19742
+ Server <= 7.119.x checks only that the root is non-empty and keeps relative roots as written; such rows take
19743
+ no part in any check on 7.120.0 — SQL backends delete them with a one-time migration at boot, while on the
19744
+ file (`local`) backend they stay in the store, are absent from the listing, cannot be revoked, and each run
19745
+ gets an `engine_notice` `permission.rule_row_dropped` naming them. Rewrite them with the project's full path.
19746
+ An EMPTY root is refused on every version (an empty prefix contains every cwd). The root's truthfulness is
19747
+ never validated — only its spelling.
19590
19748
  oneOf:
19591
19749
  - type: object
19592
19750
  additionalProperties: false
@@ -19598,7 +19756,7 @@ components:
19598
19756
  required: [kind, root]
19599
19757
  properties:
19600
19758
  kind: { type: string, enum: [project] }
19601
- 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.' }
19759
+ 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. Server >= 7.120.0: a complete absolute path (see the RuleScope description).' }
19602
19760
 
19603
19761
  RuleBehavior:
19604
19762
  type: string
@@ -19674,9 +19832,12 @@ components:
19674
19832
  cloud deployment has no access to the user's filesystem — that is the deployment shape, not a bypass:
19675
19833
  what is read is still only the allow bucket.
19676
19834
 
19677
- 🔴 `root` MUST be non-empty (an empty prefix contains every cwd — see RuleScope). Its TRUTHFULNESS is
19678
- deliberately NOT validated: the server has no such tree to check against, and pretending to validate
19679
- would be a lie. It is the user's own label for "which cwds should this rule be live under".
19835
+ 🔴 `root` follows the RuleScope rule (server >= 7.120.0: a complete absolute path; otherwise 400
19836
+ `request.body_shape` and no ticket minted — the `error` is "layers[<i>].root must be an absolute path" on server >= 7.121.0,
19837
+ naming the first refused layer, and the scope sentence on 7.120.x). It applies to ALL layers alike — a `userSettings` layer's root never becomes a scope, but it is the
19838
+ same field under the same check. On <= 7.119.x only an empty root is refused (with the body-shape sentence).
19839
+ Its TRUTHFULNESS is deliberately NOT validated: the server has no such tree to check against, and pretending
19840
+ to validate would be a lie. It is the user's own label for "which cwds should this rule be live under".
19680
19841
  # 封闭:server 侧 zod 是 `.strict()`(把 `content` 拼成 `contents` 的客户端应当场 400,而不是拿到
19681
19842
  # 一份「零候选」的预览去纳闷)。
19682
19843
  additionalProperties: false
@@ -19873,7 +20034,7 @@ components:
19873
20034
  allOf: [{ $ref: '#/components/schemas/RuleBehavior' }]
19874
20035
  description: 'Which of the three this row is. Part of the row''s IDENTITY — pass it back VERBATIM to revoke, or the delete aims at another behavior''s same-text row.'
19875
20036
  rule: { type: string, description: 'Canonical rule text — `Bash(git status)` (exact) or `Bash(git status:*)` (prefix). Pass back VERBATIM to revoke.' }
19876
- scope: { type: string, description: 'Scope discriminant string: `global`, or `project:<root>` with a NON-EMPTY root. Pass back VERBATIM to revoke.' }
20037
+ scope: { type: string, description: 'Scope discriminant string: `global`, or `project:<root>` (which roots: see RuleScope — on server >= 7.120.0 every listed project row has a complete absolute root; an older server may list relative ones). Pass back VERBATIM to revoke.' }
19877
20038
  tool:
19878
20039
  type: string
19879
20040
  # 🔴 只钉非空,**不借**输入面那条 `maxLength: 190`(server 的 `LocalImportRowSchema` 帽的是**收进来的**
@@ -19974,7 +20135,7 @@ components:
19974
20135
  type: string
19975
20136
  minLength: 1
19976
20137
  maxLength: 4104
19977
- description: 'The scope discriminant string — `global` or `project:<root>` with a NON-EMPTY root (an empty root would match every cwd). Unreadable ⇒ 400 `request.body_shape`, never guessed into `global`. Cap = "project:".length + server MAX_CWD_CHARS (8+4096), same source as RuleRevokeRequest.'
20138
+ description: 'The scope discriminant string — `global` or `project:<root>` (which roots are accepted: see RuleScope; an empty root would match every cwd). Unreadable, or a root the server does not accept ⇒ 400 `request.body_shape`, never guessed into `global`. Cap = "project:".length + server MAX_CWD_CHARS (8+4096), same source as RuleRevokeRequest.'
19978
20139
  principal: { type: string, minLength: 1, maxLength: 190, description: 'OPERATOR-ONLY cross-tenant write. Absent (or your own principal) = write your own rule. Gated exactly like the revoke half.' }
19979
20140
 
19980
20141
  RuleWriteResult:
@@ -20138,7 +20299,7 @@ components:
20138
20299
  at an `allow` that got defaulted onto the same-text `deny` deletes a refusal that was meant to stay,
20139
20300
  and hands the caller a 200. core's `sameRuleIdentity` names this exact failure.
20140
20301
  rule: { type: string, minLength: 1, maxLength: 1024, description: 'The canonical rule text, VERBATIM as `RuleListResult.rules[].rule` gave it.' }
20141
- scope: { type: string, minLength: 1, maxLength: 4104, 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. Cap = "project:".length + server MAX_CWD_CHARS (8+4096) — the 1088 previously written here was the same stale independent copy the server itself fixed (rules.ts MAX_RULE_SCOPE_CHARS 顶注), found by the A-335 anchor sweep.' }
20302
+ scope: { type: string, minLength: 1, maxLength: 4104, description: 'The scope discriminant string — `global` or `project:<root>` (which roots are accepted: see RuleScope; an empty root would match every cwd; a root the server does not accept ⇒ 400 `request.body_shape`). VERBATIM as the listing gave it. Cap = "project:".length + server MAX_CWD_CHARS (8+4096) — the 1088 previously written here was the same stale independent copy the server itself fixed (rules.ts MAX_RULE_SCOPE_CHARS 顶注), found by the A-335 anchor sweep.' }
20142
20303
  principal: { type: string, minLength: 1, maxLength: 190, description: 'OPERATOR-ONLY cross-tenant revoke. Absent (or your own principal) = revoke your own rule.' }
20143
20304
 
20144
20305
  RemovalLiveness:
@@ -20483,7 +20644,7 @@ components:
20483
20644
  description: 'The v1 CLOSED SET. A third value (e.g. `win32`) ⇒ 400 request.field_invalid — never silently accepted.'
20484
20645
  platformArch: { type: string, maxLength: 32, description: 'At most 32 **UTF-8 bytes** (producer gates byte length; `maxLength` is the looser character bound, see the unit convention).' }
20485
20646
  displayName: { type: string, maxLength: 255, description: 'Echoed back by `GET /v1/devices` as `name`, and HONESTLY ABSENT there when empty. At most 255 **UTF-8 bytes** (producer gates byte length; `maxLength` is the looser character bound).' }
20486
- workspaceRoot: { type: string, minLength: 1, maxLength: 1024, description: 'The device-side root every instruction resolves against. Required and non-empty. At most 1024 **UTF-8 bytes** (producer gates byte length; `maxLength` is the looser character bound) ⇒ over it 400 request.field_invalid (a silently truncated value is a DIFFERENT value).' }
20647
+ workspaceRoot: { type: string, minLength: 1, maxLength: 1024, description: 'The device-side root every instruction resolves against (and the `cwd` of `exec` / `execStream` when neither the session nor its delegation parents registered a working directory). Server >= 7.120.0: an ABSOLUTE POSIX path — empty, relative (`work`, `./w`) or `~/…` ⇒ 400 request.field_invalid, `error` starting "workspaceRoot must be an absolute POSIX path", the token NOT consumed (resolve the root on the device first). Server <= 7.119.x: only non-empty is checked, and a device registered with a relative root has its sessions refused when 7.120.0 builds their execution environment — re-enroll with an absolute root (new `deviceId`; revoke the old one). At most 1024 **UTF-8 bytes** (producer gates byte length; `maxLength` is the looser character bound) ⇒ over it 400 request.field_invalid (a silently truncated value is a DIFFERENT value).' }
20487
20648
 
20488
20649
  DeviceEnrollResult:
20489
20650
  type: object
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sema-agent/sdk",
3
- "version": "17.1.0",
3
+ "version": "18.0.0",
4
4
  "description": "Typed, zero-runtime-dependency SDK for the Sema agent fleet usage plane. The shared substrate for all doors (CC/Codex MCP façade + web). Server-side only — tokens never enter the browser.",
5
5
  "type": "module",
6
6
  "license": "BUSL-1.1",
@@ -52,6 +52,6 @@
52
52
  "prepublishOnly": "rm -f tsconfig.tsbuildinfo && npm run build && npm run typecheck --workspaces=false --prefix ../.. && npm test --workspaces=false --prefix ../.. && npm run prepack"
53
53
  },
54
54
  "devDependencies": {
55
- "@sema-agent/settings-schema": "^8.0.0"
55
+ "@sema-agent/settings-schema": "^9.0.0"
56
56
  }
57
57
  }