loopctl-mcp-server 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -7
- package/index.js +53 -13
- package/lib/custody-key.js +2 -2
- package/lib/delivery-loop.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -259,7 +259,7 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
|
|
|
259
259
|
| Tool | Description |
|
|
260
260
|
|---|---|
|
|
261
261
|
| `contract_story` | Agent acknowledges a story's acceptance criteria. Transitions pending -> contracted. Refused 409 `story_held` when the story's delivery stage is `escalated`, `done` or `failed` — it is not available to agents even when it reads `pending`; an escalated story becomes available again only after `resolve_escalation` sends it to `queued`. |
|
|
262
|
-
| `claim_story` | Agent claims a contracted story with pessimistic locking. Transitions contracted -> assigned. On a loopctl with claim leases the result leads with the claim's `claim_epoch` and `claimed_until`; keep the epoch for `renew_story_claim`. Both are absent on an older server. Refused 409 `story_held` when the story's delivery stage is `escalated`, `done` or `failed` — even when it reads `pending` or `contracted`; an escalated story is claimable again only after `resolve_escalation` sends it to `queued`, a done or failed one never is. |
|
|
262
|
+
| `claim_story` | Agent claims a contracted story with pessimistic locking. Transitions contracted -> assigned. On a loopctl with claim leases the result leads with the claim's `claim_epoch` and `claimed_until`; keep the epoch for `renew_story_claim`. Both are absent on an older server. Refused 409 `story_held` when the story's delivery stage is `escalated`, `done` or `failed` — even when it reads `pending` or `contracted`; an escalated story is claimable again only after `resolve_escalation` sends it to `queued`, a done or failed one never is. Refused 409 `chg_root_review_only` when the key descends from an intake source's `chg_root_dispatch_id`: chg-server's keys may only review. |
|
|
263
263
|
| `renew_story_claim` | Renew your claim's lease (`POST /stories/:id/renew-claim`, agent key). A claim not renewed before `claimed_until` (default 24 hours, `STORY_CLAIM_LEASE_SECONDS`) is released back to `pending` under you, so renew well inside it on any story held longer. A driver-placed claim — one a placement took for a runner dispatch — is capped at its dispatch deadline (`claim_lease_cap`: placed_at + `wall_clock_seconds` + `DISPATCH_LEASE_GRACE_SECONDS` at the claim, sent to the runner as `deadline_at`; moved forward to a resume's time or the runner's acceptance + `wall_clock_seconds` + the grace, only while the claim is live; never earlier than a `deadline_at` already sent), and its `claimed_until` already IS that cap: renewing it writes nothing and returns the claim as it stands, never moving `claimed_until` past it. Required: `story_id`, `claim_epoch` (from the claim). Refusals pass through: 400 epoch missing or malformed, 422 `not_claimed`, 409 `stale_claim_epoch` (the claim has ended: stop working it), 409 `not_claimant`, 409 `lease_cap_reached` (a driver-placed claim already at its cap: nothing is renewed, stop working it). Needs a loopctl server with claim leases. |
|
|
264
264
|
| `escalate_story` | Park a story you hold for a HUMAN and stop (`POST /stories/:id/escalate`, agent key). This is what an unattended session calls INSTEAD of asking a question — a headless run has no way to ask, so escalating is the affordance. Use it when the request contradicts an existing story, KB decision or documented behaviour, when it inverts or removes behaviour a previous story deliberately added, when it is a workflow change rather than a defect fix, or when it needs a business call — not for an ordinary blocker. ONE-WAY from your side: only a human moves the story off `escalated`, so on success STOP and report. Required: `story_id`, `claim_epoch`, `reason` (verbatim, treated as untrusted data, length capped by the server in codepoints — a 400 names the bound); optional `payload` object. Idempotent under the same epoch. Refusals pass through: 409 `stale_claim_epoch`, 409 `not_claimant`, 404 `unknown_story_stage`, 400 for a malformed epoch or reason. An orchestrator or user key is 403'd on purpose. |
|
|
265
265
|
| `start_story` | Agent starts work on a claimed story. Transitions assigned -> implementing. |
|
|
@@ -269,22 +269,22 @@ Epic 39 Repo Coordination Bus — a lightweight, tenant-isolated channel for age
|
|
|
269
269
|
|
|
270
270
|
| Tool | Description |
|
|
271
271
|
|---|---|
|
|
272
|
-
| `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. Accepts optional `token_usage` object. Under the LCP-1 §9.3 signed profile, pass the `claim` from `custody_sign_claim`. |
|
|
273
|
-
| `review_complete` | Record that a review has been completed for a story. Required before verify. Under the signed profile, pass the `claim` from `custody_sign_claim`. |
|
|
272
|
+
| `report_story` | Reviewer confirms the implementation is done. Transitions implementing -> reported_done. Accepts optional `token_usage` object. Under the LCP-1 §9.3 signed profile, pass the `claim` from `custody_sign_claim`. Refused 409 `chg_root_review_only` for a key descending from an intake source's `chg_root_dispatch_id`. |
|
|
273
|
+
| `review_complete` | Record that a review has been completed for a story. Required before verify. Under the signed profile, pass the `claim` from `custody_sign_claim`. On a story bound to a chg change (loopctl US-47.5), after the existing custody gates: only a key chg-server minted for the review — a strict descendant of the intake source's `chg_root_dispatch_id` — may record it (409 `chg_reviewer_required`, the root key included); `commit_sha` is REQUIRED and must be the sha of a submitted revision of the story's current change (422 `commit_sha_not_bound`); one reviewer dispatch records one verdict (409 `chg_review_already_recorded`). `commit_sha` is ignored on any other story. |
|
|
274
274
|
|
|
275
275
|
### Verification Tools (orchestrator key)
|
|
276
276
|
|
|
277
277
|
| Tool | Description |
|
|
278
278
|
|---|---|
|
|
279
|
-
| `verify_story` | Orchestrator verifies a reported_done story. Transitions reported_done -> verified. Under the signed profile, pass the `claim` from `custody_sign_claim`. |
|
|
280
|
-
| `reject_story` | Orchestrator rejects a story with a reason. The reject releases the claim, and a delivery story may be re-contracted or escalated in the same transaction. A 500 `audit_chain_append_failed` (the escalation's chain entry was refused) means the WHOLE call rolled back: the story is unchanged, and a re-run meets the same server-side condition until an operator acts. |
|
|
279
|
+
| `verify_story` | Orchestrator verifies a reported_done story. Transitions reported_done -> verified. Under the signed profile, pass the `claim` from `custody_sign_claim`. Refused 409 `chg_root_cannot_verify` for a key descending from an intake source's `chg_root_dispatch_id` (loopctl US-47.5). |
|
|
280
|
+
| `reject_story` | Orchestrator rejects a story with a reason. The reject releases the claim, and a delivery story may be re-contracted or escalated in the same transaction. A 500 `audit_chain_append_failed` (the escalation's chain entry was refused) means the WHOLE call rolled back: the story is unchanged, and a re-run meets the same server-side condition until an operator acts. Refused 409 `chg_root_cannot_verify` for a key descending from an intake source's `chg_root_dispatch_id` (loopctl US-47.5). |
|
|
281
281
|
|
|
282
282
|
### Bulk Tools (orchestrator key)
|
|
283
283
|
|
|
284
284
|
| Tool | Description |
|
|
285
285
|
|---|---|
|
|
286
|
-
| `bulk_mark_complete` | **Backfill-only.** Bulk-marks pre-existing, never-dispatched stories (pending, unassigned, no dispatch lineage) as complete in a single call. Dispatched stories are refused — they must go through the normal report → review → verify flow. |
|
|
287
|
-
| `verify_all_in_epic` | Bulk verify all reported_done, unverified stories in an epic. |
|
|
286
|
+
| `bulk_mark_complete` | **Backfill-only.** Bulk-marks pre-existing, never-dispatched stories (pending, unassigned, no dispatch lineage) as complete in a single call. Dispatched stories are refused — they must go through the normal report → review → verify flow. Refused 409 `chg_root_cannot_verify` for the whole batch for a chg-rooted key (loopctl US-47.5). |
|
|
287
|
+
| `verify_all_in_epic` | Bulk verify all reported_done, unverified stories in an epic. Refused 409 `chg_root_cannot_verify` for the whole epic for a chg-rooted key (loopctl US-47.5). |
|
|
288
288
|
|
|
289
289
|
### Token Efficiency Tools
|
|
290
290
|
|
|
@@ -664,6 +664,8 @@ loopctl enforces that nobody marks their own work as done. The API returns `409`
|
|
|
664
664
|
|
|
665
665
|
The implementer's final action is `request_review`. All subsequent steps (report, review, verify) must come from different agents.
|
|
666
666
|
|
|
667
|
+
On a story bound to a chg change (loopctl US-47.5) the review comes from chg-server: `review_complete` takes it only from a key chg-server minted beneath its registered `chg_root_dispatch_id` (409 `chg_reviewer_required`), naming the reviewed revision as `commit_sha` (422 `commit_sha_not_bound`), one verdict per reviewer dispatch (409 `chg_review_already_recorded`). chg-server's keys never certify or implement: `verify_story`, `reject_story`, `verify_all_in_epic` and `bulk_mark_complete` answer 409 `chg_root_cannot_verify`, `claim_story` and `report_story` 409 `chg_root_review_only`. A chg story that lands with no review for its landed revision is escalated over `review_missing` and is not verified without a human.
|
|
668
|
+
|
|
667
669
|
## Witness protocol (STH)
|
|
668
670
|
|
|
669
671
|
Every authenticated request echoes the caller's last-known Signed Tree Head (STH)
|
package/index.js
CHANGED
|
@@ -1372,12 +1372,15 @@ async function reportStory({ story_id, artifact_type, artifact_path, token_usage
|
|
|
1372
1372
|
return toContent(result);
|
|
1373
1373
|
}
|
|
1374
1374
|
|
|
1375
|
-
async function reviewComplete({ story_id, review_type, findings_count, fixes_count, disproved_count, summary, claim }) {
|
|
1375
|
+
async function reviewComplete({ story_id, review_type, findings_count, fixes_count, disproved_count, summary, commit_sha, claim }) {
|
|
1376
1376
|
const body = { review_type };
|
|
1377
1377
|
if (findings_count != null) body.findings_count = findings_count;
|
|
1378
1378
|
if (fixes_count != null) body.fixes_count = fixes_count;
|
|
1379
1379
|
if (disproved_count != null) body.disproved_count = disproved_count;
|
|
1380
1380
|
if (summary) body.summary = summary;
|
|
1381
|
+
// loopctl US-47.5: the reviewed chg revision. Forwarded as given and judged server-side
|
|
1382
|
+
// (422 commit_sha_not_bound), so a malformed value reaches the one place that can say why.
|
|
1383
|
+
if (commit_sha != null) body.commit_sha = commit_sha;
|
|
1381
1384
|
if (claim) body.claim = claim;
|
|
1382
1385
|
|
|
1383
1386
|
const result = await apiCall(
|
|
@@ -1400,7 +1403,7 @@ async function verifyStory({ story_id, summary, review_type, claim }) {
|
|
|
1400
1403
|
if (review_type) body.review_type = review_type;
|
|
1401
1404
|
if (claim) body.claim = claim;
|
|
1402
1405
|
|
|
1403
|
-
// `exact_role: :orchestrator` (`story_verification_controller.ex:
|
|
1406
|
+
// `exact_role: :orchestrator` (`story_verification_controller.ex:30-31`). When
|
|
1404
1407
|
// LOOPCTL_ORCH_KEY is set it is sent EXACTLY, because `resolveKey` would otherwise
|
|
1405
1408
|
// discard it in favour of a global LOOPCTL_API_KEY on the very calls it was set for.
|
|
1406
1409
|
// When it is NOT set, the global key still goes — see lib/custody-key.js for why the
|
|
@@ -1418,7 +1421,7 @@ async function verifyStory({ story_id, summary, review_type, claim }) {
|
|
|
1418
1421
|
}
|
|
1419
1422
|
|
|
1420
1423
|
async function rejectStory({ story_id, reason }) {
|
|
1421
|
-
// `exact_role: :orchestrator` (`story_verification_controller.ex:
|
|
1424
|
+
// `exact_role: :orchestrator` (`story_verification_controller.ex:30-31`) — see
|
|
1422
1425
|
// lib/custody-key.js.
|
|
1423
1426
|
const orch = orchestratorKeyArgs();
|
|
1424
1427
|
|
|
@@ -1436,7 +1439,7 @@ async function rejectStory({ story_id, reason }) {
|
|
|
1436
1439
|
|
|
1437
1440
|
async function bulkMarkComplete({ stories }) {
|
|
1438
1441
|
// stories: [{story_id, summary, review_type}]
|
|
1439
|
-
// `exact_role: :orchestrator` (`bulk_operations_controller.ex:
|
|
1442
|
+
// `exact_role: :orchestrator` (`bulk_operations_controller.ex:25-26`) — see
|
|
1440
1443
|
// lib/custody-key.js.
|
|
1441
1444
|
const orch = orchestratorKeyArgs();
|
|
1442
1445
|
|
|
@@ -1451,7 +1454,7 @@ async function bulkMarkComplete({ stories }) {
|
|
|
1451
1454
|
}
|
|
1452
1455
|
|
|
1453
1456
|
async function verifyAllInEpic({ epic_id, review_type, summary }) {
|
|
1454
|
-
// `exact_role: :orchestrator` (`story_verification_controller.ex:
|
|
1457
|
+
// `exact_role: :orchestrator` (`story_verification_controller.ex:30-31`, `:verify_all`) —
|
|
1455
1458
|
// see lib/custody-key.js.
|
|
1456
1459
|
const orch = orchestratorKeyArgs();
|
|
1457
1460
|
|
|
@@ -3454,7 +3457,7 @@ async function resolveEscalation(args) {
|
|
|
3454
3457
|
}
|
|
3455
3458
|
|
|
3456
3459
|
// #846: force-unclaim is `exact_role: :orchestrator`
|
|
3457
|
-
// (`story_verification_controller.ex:
|
|
3460
|
+
// (`story_verification_controller.ex:30-31`), so this is the one delivery-loop verb that wants
|
|
3458
3461
|
// the ORCH key. Same selection as the other four custody verbs: when LOOPCTL_ORCH_KEY is set it
|
|
3459
3462
|
// is sent EXACTLY, because `resolveKey` (`index.js:132-138`) would otherwise discard it for a
|
|
3460
3463
|
// global LOOPCTL_API_KEY on the one call it was set for; when it is not set, a global key still
|
|
@@ -4725,7 +4728,9 @@ const TOOLS = [
|
|
|
4725
4728
|
"reads pending or contracted — move on. An escalated story is claimable again only " +
|
|
4726
4729
|
"after resolve_escalation sends it to queued; a done or failed one never is. Refused " +
|
|
4727
4730
|
"409 dependencies_not_met when a story it depends on, or one in an epic its epic " +
|
|
4728
|
-
"depends on, is not verified: list_blocked_stories names them."
|
|
4731
|
+
"depends on, is not verified: list_blocked_stories names them. Refused 409 " +
|
|
4732
|
+
"chg_root_review_only when the key descends from an intake source's " +
|
|
4733
|
+
"chg_root_dispatch_id: chg-server's keys may only review.",
|
|
4729
4734
|
inputSchema: {
|
|
4730
4735
|
type: "object",
|
|
4731
4736
|
properties: {
|
|
@@ -4871,7 +4876,9 @@ const TOOLS = [
|
|
|
4871
4876
|
description:
|
|
4872
4877
|
"Reviewer (a DIFFERENT agent from the implementer) confirms the implementation is done. " +
|
|
4873
4878
|
"Chain-of-custody: the implementing agent cannot call this. " +
|
|
4874
|
-
"Transitions implementing -> reported_done. Uses the ORCH key."
|
|
4879
|
+
"Transitions implementing -> reported_done. Uses the ORCH key. Refused 409 " +
|
|
4880
|
+
"chg_root_review_only when the key descends from an intake source's " +
|
|
4881
|
+
"chg_root_dispatch_id: chg-server's keys may only review.",
|
|
4875
4882
|
inputSchema: {
|
|
4876
4883
|
type: "object",
|
|
4877
4884
|
properties: {
|
|
@@ -4906,7 +4913,16 @@ const TOOLS = [
|
|
|
4906
4913
|
name: "review_complete",
|
|
4907
4914
|
description:
|
|
4908
4915
|
"Record that a review has been completed for a story. " +
|
|
4909
|
-
"Must be called before verify_story. Uses the ORCH key."
|
|
4916
|
+
"Must be called before verify_story. Uses the ORCH key. " +
|
|
4917
|
+
"On a story BOUND TO A chg CHANGE (loopctl US-47.5) three more rules apply, after the " +
|
|
4918
|
+
"existing custody gates (self_review_blocked, caller_lineage_required, " +
|
|
4919
|
+
"unresolvable_dispatch_lineage), never instead of them: the key must be one chg-server " +
|
|
4920
|
+
"minted for the review — a strict descendant of the intake source's " +
|
|
4921
|
+
"chg_root_dispatch_id — else 409 chg_reviewer_required (chg-server's root key is " +
|
|
4922
|
+
"refused too); commit_sha is REQUIRED and must be the sha of a submitted revision of " +
|
|
4923
|
+
"the story's current change, else 422 commit_sha_not_bound; and one reviewer dispatch " +
|
|
4924
|
+
"records one verdict, a second is 409 chg_review_already_recorded. On any other story " +
|
|
4925
|
+
"commit_sha is ignored and not stored.",
|
|
4910
4926
|
inputSchema: {
|
|
4911
4927
|
type: "object",
|
|
4912
4928
|
properties: {
|
|
@@ -4935,6 +4951,13 @@ const TOOLS = [
|
|
|
4935
4951
|
type: "string",
|
|
4936
4952
|
description: "Optional: summary of the review outcome.",
|
|
4937
4953
|
},
|
|
4954
|
+
commit_sha: {
|
|
4955
|
+
type: "string",
|
|
4956
|
+
description:
|
|
4957
|
+
"REQUIRED on a story bound to a chg change: the reviewed revision commit, 40 or " +
|
|
4958
|
+
"64 lowercase hex characters, the sha of a submitted revision of the story's " +
|
|
4959
|
+
"current change (422 commit_sha_not_bound otherwise). Ignored on any other story.",
|
|
4960
|
+
},
|
|
4938
4961
|
claim: CLAIM_SCHEMA,
|
|
4939
4962
|
},
|
|
4940
4963
|
required: ["story_id", "review_type"],
|
|
@@ -4947,7 +4970,15 @@ const TOOLS = [
|
|
|
4947
4970
|
description:
|
|
4948
4971
|
"Orchestrator verifies a reported_done story. " +
|
|
4949
4972
|
"Requires a review_record to exist (call review_complete first). " +
|
|
4950
|
-
"Transitions reported_done -> verified. Uses the ORCH key."
|
|
4973
|
+
"Transitions reported_done -> verified. Uses the ORCH key. Refused 409 " +
|
|
4974
|
+
"chg_root_cannot_verify when the key descends from an intake source's " +
|
|
4975
|
+
"chg_root_dispatch_id (loopctl US-47.5): chg-server's keys review and never verify — " +
|
|
4976
|
+
"use an orchestrator key outside chg's tree. A story BOUND TO A chg CHANGE is verified " +
|
|
4977
|
+
"only on a chg review of its LANDED revision (the revision commit of the newest " +
|
|
4978
|
+
"applied landed event of its current change), else 409 " +
|
|
4979
|
+
"chg_review_not_for_landed_revision — have chg dispatch a review naming that sha; and " +
|
|
4980
|
+
"never while its stage is escalated over review_missing, else 409 " +
|
|
4981
|
+
"review_missing_escalated — a human resolves it first (resolve_escalation).",
|
|
4951
4982
|
inputSchema: {
|
|
4952
4983
|
type: "object",
|
|
4953
4984
|
properties: {
|
|
@@ -4976,7 +5007,10 @@ const TOOLS = [
|
|
|
4976
5007
|
"The reject releases the claim, and a delivery story may be re-contracted or escalated " +
|
|
4977
5008
|
"in the same transaction. A 500 `audit_chain_append_failed` (the escalation's chain " +
|
|
4978
5009
|
"entry was refused) means the WHOLE call rolled back: the story is unchanged, and a " +
|
|
4979
|
-
"re-run meets the same server-side condition until an operator acts."
|
|
5010
|
+
"re-run meets the same server-side condition until an operator acts. Refused 409 " +
|
|
5011
|
+
"chg_root_cannot_verify when the key descends from an intake source's " +
|
|
5012
|
+
"chg_root_dispatch_id (loopctl US-47.5): reject is a custody decision chg-server's " +
|
|
5013
|
+
"keys never make.",
|
|
4980
5014
|
inputSchema: {
|
|
4981
5015
|
type: "object",
|
|
4982
5016
|
properties: {
|
|
@@ -4999,7 +5033,9 @@ const TOOLS = [
|
|
|
4999
5033
|
description:
|
|
5000
5034
|
"Bulk mark multiple stories as complete (backfill-only for never-dispatched work). " +
|
|
5001
5035
|
"ADMIN USE ONLY: backfill pre-existing completed work into pending stories that never entered the dispatch lifecycle. " +
|
|
5002
|
-
"For dispatched stories, use the normal report/review/verify flow. Each story entry needs a story_id, summary, and review_type. Uses the ORCH key."
|
|
5036
|
+
"For dispatched stories, use the normal report/review/verify flow. Each story entry needs a story_id, summary, and review_type. Uses the ORCH key. " +
|
|
5037
|
+
"Refused 409 chg_root_cannot_verify for the whole batch when the key descends from an intake source's chg_root_dispatch_id (loopctl US-47.5). " +
|
|
5038
|
+
"A story bound to a chg change is a per-story error leading with review_missing_escalated or chg_review_not_for_landed_revision, the same landed-review gate verify_story applies.",
|
|
5003
5039
|
inputSchema: {
|
|
5004
5040
|
type: "object",
|
|
5005
5041
|
properties: {
|
|
@@ -5033,7 +5069,11 @@ const TOOLS = [
|
|
|
5033
5069
|
name: "verify_all_in_epic",
|
|
5034
5070
|
description:
|
|
5035
5071
|
"Bulk verify all reported_done, unverified stories in an epic. " +
|
|
5036
|
-
"Convenience endpoint for the orchestrator after a review pass. Uses the ORCH key."
|
|
5072
|
+
"Convenience endpoint for the orchestrator after a review pass. Uses the ORCH key. " +
|
|
5073
|
+
"Refused 409 chg_root_cannot_verify for the whole epic, verifying nothing, when the key " +
|
|
5074
|
+
"descends from an intake source's chg_root_dispatch_id (loopctl US-47.5). A story bound " +
|
|
5075
|
+
"to a chg change is skipped into errors with review_missing_escalated or " +
|
|
5076
|
+
"chg_review_not_for_landed_revision, the same landed-review gate verify_story applies.",
|
|
5037
5077
|
inputSchema: {
|
|
5038
5078
|
type: "object",
|
|
5039
5079
|
properties: {
|
package/lib/custody-key.js
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Key selection for the `exact_role: :orchestrator` custody verbs (loopctl #846 review round 2).
|
|
3
3
|
*
|
|
4
4
|
* WHICH VERBS. `verify`, `reject` and `force_unclaim` plus the aggregate `verify_all`
|
|
5
|
-
* (`lib/loopctl_web/controllers/story_verification_controller.ex:
|
|
5
|
+
* (`lib/loopctl_web/controllers/story_verification_controller.ex:30-31`), and bulk
|
|
6
6
|
* `verify`/`reject`/`mark_complete`
|
|
7
|
-
* (`lib/loopctl_web/controllers/bulk_operations_controller.ex:
|
|
7
|
+
* (`lib/loopctl_web/controllers/bulk_operations_controller.ex:25-26`). All are mounted
|
|
8
8
|
* `exact_role: :orchestrator`, where the role hierarchy does NOT apply: `RequireRole`'s
|
|
9
9
|
* exact-role clause tests `api_key.role == exact_role` (`require_role.ex:65-75`), so a `:user`
|
|
10
10
|
* or `:superadmin` key is refused there exactly as an `:agent` key is.
|
package/lib/delivery-loop.js
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* - `resolve_escalation` is the HUMAN half of the escalation pair: a `:user` key no dispatch
|
|
22
22
|
* minted. The agent key that raises an escalation is 403'd on it by design.
|
|
23
23
|
* - `force_unclaim_story` is gated `exact_role: :orchestrator`
|
|
24
|
-
* (`story_verification_controller.ex:
|
|
24
|
+
* (`story_verification_controller.ex:30-31`), so it needs an ORCHESTRATOR-ROLE key and a
|
|
25
25
|
* higher-privileged one does NOT substitute: `RequireRole`'s exact-role clause tests
|
|
26
26
|
* `api_key.role == exact_role` (`require_role.ex:65-75`), so a `:user` or `:superadmin`
|
|
27
27
|
* key is 403'd there exactly as an agent key is. That is a chain-of-custody gate, not an
|
|
@@ -293,7 +293,7 @@ export async function resolveEscalation({ story_id, to, reason } = {}, { userKey
|
|
|
293
293
|
* No request body — the story is named in the path.
|
|
294
294
|
*
|
|
295
295
|
* ORCHESTRATOR ROLE, and exactly that: the action is `exact_role: :orchestrator`
|
|
296
|
-
* (`story_verification_controller.ex:
|
|
296
|
+
* (`story_verification_controller.ex:30-31`), so a user or superadmin key is refused —
|
|
297
297
|
* `RequireRole`'s exact-role clause tests `api_key.role == exact_role`
|
|
298
298
|
* (`require_role.ex:65-75`).
|
|
299
299
|
*
|