@workerkit/core 0.3.13 → 0.3.15

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.
@@ -1,4 +1,4 @@
1
- // The 95 authenticated fleet-management tools, as pure descriptors. The
1
+ // The 97 authenticated fleet-management tools, as pure descriptors. The
2
2
  // descriptions ARE the product surface: they are served verbatim to MCP
3
3
  // clients (and any future CLI help), so every contract nuance an agent must
4
4
  // not get wrong is taught here.
@@ -7,6 +7,7 @@ import { appDiscoveryDescriptors } from "./app-discovery.js";
7
7
  import { compactDecisionReceipt } from "./decision-receipt.js";
8
8
  import { createDecisionWorker } from "./decision-authoring.js";
9
9
  import { onboardingDescriptors } from "./onboarding.js";
10
+ import { supportDescriptors } from "./support.js";
10
11
  import { z } from "../zod.js";
11
12
  import { CREATE, DELETE, READ_ONLY, TRIGGER, UPDATE } from "./types.js";
12
13
  // The graded-outcome vocabulary, taught wherever a score can be read or written.
@@ -15,8 +16,8 @@ import { CREATE, DELETE, READ_ONLY, TRIGGER, UPDATE } from "./types.js";
15
16
  const SCORE_NOTE = " Grading: selfScore (0-100) is the run's OWN assessment of how much of its task it accomplished — a self-report, not a measurement; ownerScore (0-100) is the human's grade and OVERRIDES it wherever both exist; selfScoreReason explains selfScore in one line. All three are null when the run was never assessed (stateless workers and very short runs are not graded): null means NOT ASSESSED, never zero — never report it as 0 or average nulls into a success rate.";
16
17
  const API = "/api/manage/workers";
17
18
  // ─── Shared phrasing ────────────────────────────────────────────────────────
18
- const TOKEN_ID_HINT = "The worker's numeric tokenId, as returned by workers_list. 404 not_found = no such worker on this account (another account's is indistinguishable by design).";
19
- const RUN_ID_HINT = "The run's UUID, as returned by worker_run (runId), workers_list (currentRunId / lastRun.runId), or worker_runs.";
19
+ const TOKEN_ID_HINT = "The worker's numeric tokenId, as returned by workers_list. 404 not_found = no such worker on this account, or one outside this key's worker selection (another account's and an unselected one are indistinguishable by design; key_info says which workers the key reaches).";
20
+ const RUN_ID_HINT = "The run's UUID, as returned by worker_run (runId), workers_list (currentRunId / lastRun.runId), or worker_runs. 404 not_found = no such run, or its worker is outside this key's worker selection.";
20
21
  const DECISION_PROFILE_HINT = "Classifier choice: a profileId from models_list.decisionProfiles, independent of the language model. Platform-funded for decision and hybrid workers. Review sample results and confidence thresholds when changing providers. Omit to keep the configured classifier (Jev by default).";
21
22
  // The whole runs family shares this degradation mode; teach it once per tool so
22
23
  // agents never retry-loop a 422.
@@ -31,12 +32,12 @@ const HYBRID_NOTE = " Hybrid workers classify a bounded source window, then run
31
32
  // existed that day. Taught on key_info and on every tool behind a scope that is
32
33
  // younger than the surface, so a 403 there is read as "re-scope the key", not
33
34
  // "retry".
34
- const NEW_SCOPE_NOTE = " A key minted before this scope existed does not carry it (even one minted with \"all\"): a 403 here is fixed by an account admin re-scoping or re-minting the key at https://workerkit.ai, never by retrying — check key_info's scopes first.";
35
+ const NEW_SCOPE_NOTE = " A key minted before this scope existed does not carry it (even one minted with \"all\"): ask its owner or an account Admin to review its scope and authority at https://workerkit.ai, never retry automatically — check key_info's scopes first.";
35
36
  // ─── Key ────────────────────────────────────────────────────────────────────
36
37
  const getKeyInfo = {
37
38
  name: "key_info",
38
39
  title: "Key Info",
39
- description: "What the presented manager key IS: accountId, accountTitle, keyName, keyPrefix (a display prefix — never the key itself), scopes, expiresAt, and serverTimeUtc. CALL THIS FIRST, before planning any work: the scopes list is exactly what the other tools on this surface will accept, so read it up front rather than discovering a wall mid-task. Any valid key may ask — this is the one tool that needs NO scope. A call outside the list fails 403 OPERATION_NOT_ALLOWED naming the missing scope, and only an account admin can add one (re-scope or re-mint the key at https://workerkit.ai) — never retry a 403. A scope added to the product AFTER a key was minted does NOT reach that key, INCLUDING a key minted with \"all\" (which stored the bitmask of the scopes that existed that day), so a tool can 403 on a key its owner believes has everything: this list is the only truth about it. Compute expiry and elapsed times against serverTimeUtc, never your own clock.",
40
+ description: "What the presented manager key IS: accountId, accountTitle, subjectUserId, accountWideAuthority, keyName, keyPrefix (a display prefix — never the key itself), scopes, allWorkers, workerIds, expiresAt, and serverTimeUtc. accountWideAuthority:false limits all permissions to the subject member's own workers. Membership removal or role change invalidates the key and requires new consent. Admin-only setup operations remain unavailable to member keys, regardless of scopes. CALL THIS FIRST, before planning any work: the scopes list is exactly what the other tools on this surface will accept, so read it up front rather than discovering a wall mid-task. allWorkers false means the key reaches only workerIds (plus any worker it creates): workers_list, fleet_health, runs_feed, fleet_pulse and deployments_list then show those workers only, and any other worker or run id is 404 not_found exactly like one that does not exist — the credential owner or an account Admin can change the selection at https://workerkit.ai, within the subject's live authority. Any valid key may ask — this is the one tool that needs NO scope. A call outside the list fails 403 OPERATION_NOT_ALLOWED naming the missing scope, and the credential owner or an account Admin can change its scopes at https://workerkit.ai within the existing grant ceiling — never retry a 403. A scope added to the product AFTER a key was minted does NOT reach that key, INCLUDING a key minted with \"all\" (which stored the bitmask of the scopes that existed that day), so a tool can 403 on a key its owner believes has everything: this list is the only truth about it. Compute expiry and elapsed times against serverTimeUtc, never your own clock.",
40
41
  auth: "manager",
41
42
  method: "get",
42
43
  schema: {},
@@ -47,7 +48,7 @@ const getKeyInfo = {
47
48
  const listWorkers = {
48
49
  name: "workers_list",
49
50
  title: "List Workers",
50
- description: "List the AI workers on this account with live status: title, avatar, status (active|paused|expired), isEnabled, expiresAt, readiness (status ready|blocked with the actionable issues — the same block worker_get carries), modelType (language | decision | hybrid | none — see worker_get), deployment (null = this worker is NOT on the hosted runtime and will never run, whatever readiness says; worker_deploy fixes that — except on an Action worker, modelType none, which is never deployed or run: worker_tools lists its tools and worker_tool_call calls one), lastRun (the newest SETTLED run: runId, outcome succeeded|attention|failed|awaitingInput|skipped|canceled, skipReason, errorCode — in-flight runs never appear here), schedule rollup (total/enabled/nextRunUtc), and live state (isRunning, inFlightRuns, currentRunId — poll run_get with currentRunId to watch it). FILTERS, all optional and ANDed: status, deployed, readiness, q (title substring); an unknown status/readiness value is a 400 invalid_filter, never an empty page. totalWorkers is the count BEFORE filtering: 0 means nothing is installed on this account yet (shortlist a kit with kits_search on the Directory server, kit_install it with deploy:true), non-zero with an empty workers[] means the filter matched nothing. For 'what is wrong with this fleet' call fleet_health instead of scanning this list. Compute elapsed/next-run times against the response's serverTimeUtc, never your own clock. IMPORTANT: lastRun.resultText is the report the worker itself wrote and requires the manager key to hold the readRuns scope IN ADDITION to readWorkers — with readWorkers only it is null, which does NOT mean the run produced no report; check your key's scopes before concluding anything from a null. Scope model for every tool here: a 403 OPERATION_NOT_ALLOWED names the missing scope — the key must be re-minted with broader scopes by an account admin at https://workerkit.ai (do not retry the call).",
51
+ description: "List the AI workers on this account with live status: title, avatar, status (active|paused|expired), isEnabled, expiresAt, readiness (status ready|blocked with the actionable issues — the same block worker_get carries), modelType (language | decision | hybrid | none — see worker_get), deployment (null = this worker is NOT on the hosted runtime and will never run, whatever readiness says; worker_deploy fixes that — except on an Action worker, modelType none, which is never deployed or run: worker_tools lists its tools and worker_tool_call calls one), lastRun (the newest SETTLED run: runId, outcome succeeded|attention|failed|awaitingInput|skipped|canceled, skipReason, errorCode — in-flight runs never appear here), schedule rollup (total/enabled/nextRunUtc), and live state (isRunning, inFlightRuns, currentRunId — poll run_get with currentRunId to watch it). FILTERS, all optional and ANDed: status, deployed, readiness, q (title substring); an unknown status/readiness value is a 400 invalid_filter, never an empty page. totalWorkers is the count BEFORE filtering, over the workers this key reaches: 0 means nothing the key can reach is installed yet (key_info says whether the key is limited to selected workers; shortlist a kit with kits_search on the Directory server and kit_install it with deploy:true — a key limited to selected workers gains the worker it installs), non-zero with an empty workers[] means the filter matched nothing. For 'what is wrong with this fleet' call fleet_health instead of scanning this list. Compute elapsed/next-run times against the response's serverTimeUtc, never your own clock. IMPORTANT: lastRun.resultText is the report the worker itself wrote and requires the manager key to hold the readRuns scope IN ADDITION to readWorkers — with readWorkers only it is null, which does NOT mean the run produced no report; check your key's scopes before concluding anything from a null. Scope model for every tool here: a 403 OPERATION_NOT_ALLOWED names the missing scope — the key must be re-minted with broader scopes by an account admin at https://workerkit.ai (do not retry the call).",
51
52
  auth: "manager",
52
53
  method: "get",
53
54
  schema: {
@@ -55,6 +56,7 @@ const listWorkers = {
55
56
  deployed: z.boolean().optional().describe("true = only workers on the hosted runtime (the ones that can run); false = only workers without a deployment, which includes every Action worker (modelType none: never deployed, called with worker_tool_call). Omit for both."),
56
57
  readiness: z.enum(["ready", "blocked"]).optional().describe("ready = configuration lets it run hosted; blocked = something actionable is missing (an app connection, an instruction, an identity). Omit for both."),
57
58
  q: z.string().max(100).optional().describe("Case-insensitive substring of the worker's title."),
59
+ ownerUserId: z.number().int().positive().optional().describe("Filter by ownerUserId from a worker row. This only narrows the workers the key already reaches."),
58
60
  },
59
61
  path: API,
60
62
  annotations: READ_ONLY,
@@ -131,7 +133,7 @@ const runWorkersBulk = {
131
133
  strictInput: true,
132
134
  name: "run_bulk",
133
135
  title: "Run Workers in Bulk",
134
- description: "Run several workers now in ONE call — a prompt fanned out across a fleet without spending the single trigger's 30/min budget one worker at a time. Up to 20 workers per call, each named by workerId (preferred) or its deprecated tokenId (the GUID wins when both are sent), each with its own optional prompt, modelSlug, decisionProfileId, sourceArgs, answers and maxItems. IT IS NOT ATOMIC AND IT DOES NOT STOP: every worker passes the mint gauntlet on its own, so an unknown id, a missing deployment or a cap already hit is reported on THAT item (success false, errorCode, error) and the loop goes on to the next — a stale id in a fan-out never discards the nineteen runs that were fine. A SKIPPED RECEIPT IS A SUCCESS HERE: a run was minted and its status/skipReason say why it did not start, so read items[] and never the status code or minted alone. Each successful item carries runId, status, skipReason and errorCode; run_events and run_get take the runId from there. Recorded as API-triggered runs, attributed to the manager key's minter. Own window: 2 calls per minute and 20 per hour per ACCOUNT, on top of the surface windows. Requires the runWorkers scope." +
136
+ description: "Run several workers now in ONE call — a prompt fanned out across a fleet without spending the single trigger's 30/min budget one worker at a time. Up to 20 workers per call, each named by workerId (preferred) or its deprecated tokenId (the GUID wins when both are sent), each with its own optional prompt, modelSlug, decisionProfileId, sourceArgs, answers and maxItems. IT IS NOT ATOMIC AND IT DOES NOT STOP: every worker passes the mint gauntlet on its own, so an unknown id (or one outside this key's worker selection, which reads the same), a missing deployment or a cap already hit is reported on THAT item (success false, errorCode, error) and the loop goes on to the next — a stale id in a fan-out never discards the nineteen runs that were fine. A SKIPPED RECEIPT IS A SUCCESS HERE: a run was minted and its status/skipReason say why it did not start, so read items[] and never the status code or minted alone. Each successful item carries runId, status, skipReason and errorCode; run_events and run_get take the runId from there. Recorded as API-triggered runs, attributed to the manager key's minter. Own window: 2 calls per minute and 20 per hour per ACCOUNT, on top of the surface windows. Requires the runWorkers scope." +
135
137
  RUNTIME_NOTE,
136
138
  auth: "manager",
137
139
  method: "post",
@@ -157,12 +159,13 @@ const runWorkersBulk = {
157
159
  };
158
160
  // ─── Fleet-wide run reads ───────────────────────────────────────────────────
159
161
  // The two calls that watch a WHOLE fleet without fanning out per worker. Both are
160
- // account-scoped by the key itself, so neither takes a worker id, and both are
161
- // deliberately money-free: cost lives on the run receipt.
162
+ // scoped by the key itself — its account, and its worker selection when it has
163
+ // one — so neither takes a worker id, and both are deliberately money-free: cost
164
+ // lives on the run receipt.
162
165
  const runsFeed = {
163
166
  name: "runs_feed",
164
167
  title: "Account Run Feed",
165
- description: "Every worker's runs in ONE account-wide feed, newest first — THE fleet-watching call. When you are minding many workers, call this instead of looping workers_list or worker_runs per worker. Each row is a scan line: runId, workerId, tokenTitle, status, outcome (running|succeeded|attention|failed|awaitingInput|skipped|canceled — 'attention' is a SUCCEEDED run that hit friction, i.e. firewall denials, tool errors or reported issues, so report it apart from a clean success; 'awaitingInput' is a run that ended by asking its owner a question — waiting on a person, not broken: run_question reads it, run_answer answers it, and fleet_health lists every open one), skipReason, errorCode, triggerKind, scheduleTitle, timings, the friction counts (toolCalls, firewallDenials, toolErrors, issueCount, hasBlockerIssue), and one line of what the run did in summary + summarySource ('distilled' = a condensed record of what the run ACTUALLY did; 'report' = the worker's OWN closing account; 'error'/'skipped'/'activity' = the machine's own words — these are not equally reliable, so say which one you are quoting). Carries NO cost and NO token counts by design: read those from run_get on the one run that matters. Cursor-paged, never page-numbered — scroll by passing the previous response's nextCursor back as cursor (hasMore says whether another page exists), and re-fetch with NO cursor to refresh the head; there is deliberately no total count. A cursor we did not issue is a 400 invalid_cursor — drop it and reload the head. An unknown status is a 400 invalid_status, never a silently ignored filter. Requires the readRuns scope." +
168
+ description: "Every reachable worker's runs in ONE feed (the whole account for an all-workers key), newest first — THE fleet-watching call. When you are minding many workers, call this instead of looping workers_list or worker_runs per worker. Each row is a scan line: runId, workerId, tokenTitle, status, outcome (running|succeeded|attention|failed|awaitingInput|skipped|canceled — 'attention' is a SUCCEEDED run that hit friction, i.e. firewall denials, tool errors or reported issues, so report it apart from a clean success; 'awaitingInput' is a run that ended by asking its owner a question — waiting on a person, not broken: run_question reads it, run_answer answers it, and fleet_health lists every open one), skipReason, errorCode, triggerKind, scheduleTitle, timings, the friction counts (toolCalls, firewallDenials, toolErrors, issueCount, hasBlockerIssue), and one line of what the run did in summary + summarySource ('distilled' = a condensed record of what the run ACTUALLY did; 'report' = the worker's OWN closing account; 'error'/'skipped'/'activity' = the machine's own words — these are not equally reliable, so say which one you are quoting). Carries NO cost and NO token counts by design: read those from run_get on the one run that matters. Cursor-paged, never page-numbered — scroll by passing the previous response's nextCursor back as cursor (hasMore says whether another page exists), and re-fetch with NO cursor to refresh the head; there is deliberately no total count. A cursor we did not issue is a 400 invalid_cursor — drop it and reload the head. An unknown status is a 400 invalid_status, never a silently ignored filter. Requires the readRuns scope." +
166
169
  RUNTIME_NOTE,
167
170
  auth: "manager",
168
171
  method: "get",
@@ -185,7 +188,7 @@ const runsFeed = {
185
188
  const fleetPulse = {
186
189
  name: "fleet_pulse",
187
190
  title: "Fleet Pulse",
188
- description: "Every run IN FLIGHT on the account right now, in one call — the other fleet-watching call, and the single authority for \"who is working\". Use it instead of sweeping workers_list for isRunning, or worker_runs per worker. Returns serverTimeUtc plus activeRuns[]: runId, workerId, tokenTitle, status (Pending | Dispatched | Running — a QUEUED run is not yet working, so never say \"running for 3m\" about one), createDate, startedAtUtc (null while Pending/Dispatched, so render elapsed from startedAtUtc ?? createDate and label the queued case differently), triggerKind, scheduleTitle, activitySummary, lastTool + toolActive (true = the run is INSIDE that tool — 'using X'; false = that call has ended and the name is the trailing record — 'last used X'), recentToolCalls[] (newest first, ≤25, each with a seq: accumulate across polls by appending anything whose seq beats the highest you kept for that run), toolCallCount (every call the run has made — if your kept list is shorter, the run out-ran the window and the rest is in run_events), activityUpdatedAtUtc, and liveness (live|stalled while Running). ALWAYS compute elapsed times against the response's serverTimeUtc, never your own clock. Capped at 100 rows with NO paging — truncated:true says the cap bit; an empty activeRuns[] is the normal, healthy answer, not an error or a failure to look. No cost fields: money lives on the run receipt (run_get). Requires the readRuns scope." +
191
+ description: "Every run IN FLIGHT right now on the workers this key reaches, in one call — the other fleet-watching call, and the single authority for \"who is working\". Use it instead of sweeping workers_list for isRunning, or worker_runs per worker. Returns serverTimeUtc plus activeRuns[]: runId, workerId, tokenTitle, status (Pending | Dispatched | Running — a QUEUED run is not yet working, so never say \"running for 3m\" about one), createDate, startedAtUtc (null while Pending/Dispatched, so render elapsed from startedAtUtc ?? createDate and label the queued case differently), triggerKind, scheduleTitle, activitySummary, lastTool + toolActive (true = the run is INSIDE that tool — 'using X'; false = that call has ended and the name is the trailing record — 'last used X'), recentToolCalls[] (newest first, ≤25, each with a seq: accumulate across polls by appending anything whose seq beats the highest you kept for that run), toolCallCount (every call the run has made — if your kept list is shorter, the run out-ran the window and the rest is in run_events), activityUpdatedAtUtc, and liveness (live|stalled while Running). ALWAYS compute elapsed times against the response's serverTimeUtc, never your own clock. Capped at 100 rows with NO paging — truncated:true says the cap bit; an empty activeRuns[] is the normal, healthy answer, not an error or a failure to look. No cost fields: money lives on the run receipt (run_get). Requires the readRuns scope." +
189
192
  RUNTIME_NOTE,
190
193
  auth: "manager",
191
194
  method: "get",
@@ -413,6 +416,20 @@ const deleteMemoryItem = {
413
416
  annotations: DELETE,
414
417
  };
415
418
  // ─── Schedules ──────────────────────────────────────────────────────────────
419
+ const setWorkerTimeZone = {
420
+ name: "worker_timezone_set",
421
+ title: "Set Worker Timezone",
422
+ description: "Set the worker's own timezone. It decides the worker's local dates, the local times in its app results (calendar events, emails and other apps) and the times in the reports it writes. Use the owner's intended zone, for example America/New_York for Eastern time. Recomputes the next run of every daily schedule that follows the worker; explicit schedule timezone overrides remain unchanged, and a schedule's timezone never changes the worker's. Reports already saved keep their times. Requires the manageSchedules scope. Returns timeZoneId, effectiveTimeZoneId and schedules.",
423
+ auth: "manager",
424
+ method: "put",
425
+ schema: {
426
+ tokenId: z.number().int().min(1).describe(TOKEN_ID_HINT),
427
+ timeZoneId: z.string().max(64).nullable().describe("IANA timezone, such as America/New_York. Required: send null or an empty string to choose UTC."),
428
+ },
429
+ path: (params) => `${API}/${params.tokenId}/timezone`,
430
+ bodyBuilder: (params) => ({ timeZoneId: params.timeZoneId }),
431
+ annotations: UPDATE,
432
+ };
416
433
  const listSchedules = {
417
434
  name: "schedules_list",
418
435
  title: "List Schedules",
@@ -435,11 +452,16 @@ const SCHEDULE_GATE_HINT = " Two plan walls return 402 {error:'feature_gated', f
435
452
  // never overlaps itself, whatever the plan allows: two copies of the same
436
453
  // scheduled job side by side duplicate its side effects.
437
454
  const SCHEDULE_OVERLAP_NOTE = " A schedule never overlaps itself: a fire that finds any run of the worker in flight lands as a Skipped receipt (skipReason ConcurrencyLimit) whatever the plan's per-worker concurrency allows — space the schedule wider than a run takes. Runs started on demand may run alongside a scheduled one, up to the plan's limit.";
455
+ // A schedule's zone and the worker's zone are separate settings. Taught on both
456
+ // schedule writes, so an agent fixing a report's times does not reach for the
457
+ // schedule: that only moves when it fires.
458
+ const SCHEDULE_ZONE_NOTE = " A schedule's own timeZoneId only decides when that schedule fires: the worker's local dates and reports follow the worker's timezone, which worker_timezone_set changes.";
438
459
  const createSchedule = {
439
460
  name: "schedule_create",
440
461
  title: "Create Schedule",
441
462
  description: "Create a schedule for the worker. Per-type required fields: " + SCHEDULE_TYPE_HINT +
442
- " Omit timeZoneId to follow the worker's own time zone (the normal case). An Action worker (modelType none) takes no schedules: it is called, never run. Requires the manageSchedules scope." +
463
+ " Omit timeZoneId to follow the worker's own time zone (the normal case)." + SCHEDULE_ZONE_NOTE +
464
+ " An Action worker (modelType none) takes no schedules: it is called, never run. Requires the manageSchedules scope." +
443
465
  SCHEDULE_GATE_HINT +
444
466
  SCHEDULE_OVERLAP_NOTE,
445
467
  auth: "manager",
@@ -469,7 +491,8 @@ const createSchedule = {
469
491
  const updateSchedule = {
470
492
  name: "schedule_update",
471
493
  title: "Update Schedule",
472
- description: "Edit a schedule (partial — omitted fields stay unchanged; timeZoneId='' clears the override so the schedule follows the worker's zone again). nextRunUtc is recomputed on every edit. Per-type field rules: " +
494
+ description: "Edit a schedule (partial — omitted fields stay unchanged; timeZoneId='' clears the override so the schedule follows the worker's zone again). nextRunUtc is recomputed on every edit." +
495
+ SCHEDULE_ZONE_NOTE + " Per-type field rules: " +
473
496
  SCHEDULE_TYPE_HINT + " Requires manageSchedules." + SCHEDULE_GATE_HINT + SCHEDULE_OVERLAP_NOTE,
474
497
  auth: "manager",
475
498
  method: "patch",
@@ -765,7 +788,7 @@ const restoreInstructionVersion = {
765
788
  const setWorkerEnabled = {
766
789
  name: "worker_set_enabled",
767
790
  title: "Start or Stop Worker",
768
- description: "Start (enabled=true) or stop (enabled=false) a worker. Stopping pauses the WHOLE worker: its own API key stops working AND its schedules stop firing; re-enabling restores both. THIS IS THE REVERSIBLE ONE — reach for it whenever someone says stop, pause, turn off or disable; worker_delete is permanent and nothing undoes it. Two things about stopping an ORCHESTRATOR that the response does not tell you: it CASCADES to that worker's sub-workers (children and grandchildren), and re-enabling the parent brings back ONLY the parent — each sub-worker needs its own worker_set_enabled(tokenId, true), so check workers_list afterwards rather than assuming the fleet came back. And re-enabling consumes a worker slot: at the plan's worker cap it is refused with 402 limit_exceeded, so a worker stopped before a downgrade can be stuck stopped until the owner upgrades or deletes another worker. Returns {tokenId, isEnabled}. Idempotent. Requires the manageState scope.",
791
+ description: "Start (enabled=true) or stop (enabled=false) a worker. Stopping pauses the WHOLE worker: its own API key stops working AND its schedules stop firing; re-enabling restores both. THIS IS THE REVERSIBLE ONE — reach for it whenever someone says stop, pause, turn off or disable; worker_delete is permanent and nothing undoes it. Two things about stopping an ORCHESTRATOR that the response does not tell you: it CASCADES to that worker's sub-workers (children and grandchildren), and re-enabling the parent brings back ONLY the parent — each sub-worker needs its own worker_set_enabled(tokenId, true), so check workers_list afterwards rather than assuming the fleet came back. And re-enabling consumes a worker slot: at the plan's worker cap it is refused with 402 limit_exceeded, so a worker stopped before a downgrade can be stuck stopped until the owner upgrades or deletes another worker. A key limited to selected workers is refused with 409 sub_workers_outside_key when stopping would cascade to a sub-worker outside its selection — an account Admin adds those sub-workers to the key at https://workerkit.ai, or stops them separately. Returns {tokenId, isEnabled}. Idempotent. Requires the manageState scope.",
769
792
  auth: "manager",
770
793
  method: "post",
771
794
  schema: {
@@ -779,7 +802,7 @@ const setWorkerEnabled = {
779
802
  const deleteWorker = {
780
803
  name: "worker_delete",
781
804
  title: "Delete Worker",
782
- description: "Delete a worker PERMANENTLY. Not reversible by any call, on any surface: its own pe_ key stops working immediately (an agent holding it starts failing), its schedules stop firing, its instruction, memory, deployment and delivery destinations go with it, and it disappears from this API, MCP, the CLI and the dashboard alike. TO STOP A WORKER YOU MIGHT WANT BACK, USE worker_set_enabled WITH enabled:false — that pauses the key and the schedules together and is reversible (with two caveats it states: stopping cascades to sub-workers and re-enabling brings back only the worker you name, and re-enabling is refused with 402 at the plan's worker cap); THIS one cannot be undone at all, so confirm with the person before calling it, never infer it from 'get rid of', 'turn off' or 'stop'. THE SURPRISE: deleting an orchestrator DELETES ITS SUB-WORKERS TOO (children and grandchildren — a dead orchestrator must never leave live workers behind), and the response's subWorkersDeleted says how many went with it; report that number, because those workers had their own jobs. What SURVIVES: the apps stay connected for every other worker on the operator, and the run receipts stay readable through the account-wide reads (runs_feed, run_get) — a deleted worker's spending is still part of the account's history. A run in flight settles normally and is neither cancelled nor refunded; runsInFlight reports how many were running when the delete landed. Deleting FREES A WORKER SLOT, which is the fix for kit_install's 402 limit_exceeded when the owner would rather not upgrade. 404 means the worker does not exist or is not on this account — including a second delete of one already gone, so a 404 on a retry means the first call worked. Requires the deleteWorkers scope." +
805
+ description: "Delete a worker PERMANENTLY. Not reversible by any call, on any surface: its own pe_ key stops working immediately (an agent holding it starts failing), its schedules stop firing, its instruction, memory, deployment and delivery destinations go with it, and it disappears from this API, MCP, the CLI and the dashboard alike. TO STOP A WORKER YOU MIGHT WANT BACK, USE worker_set_enabled WITH enabled:false — that pauses the key and the schedules together and is reversible (with two caveats it states: stopping cascades to sub-workers and re-enabling brings back only the worker you name, and re-enabling is refused with 402 at the plan's worker cap); THIS one cannot be undone at all, so confirm with the person before calling it, never infer it from 'get rid of', 'turn off' or 'stop'. THE SURPRISE: deleting an orchestrator DELETES ITS SUB-WORKERS TOO (children and grandchildren — a dead orchestrator must never leave live workers behind), and the response's subWorkersDeleted says how many went with it; report that number, because those workers had their own jobs. What SURVIVES: the apps stay connected for every other worker on the operator, and the run receipts stay readable through the account-wide reads (runs_feed, run_get) — a deleted worker's spending is still part of the account's history. A run in flight settles normally and is neither cancelled nor refunded; runsInFlight reports how many were running when the delete landed. Deleting FREES A WORKER SLOT, which is the fix for kit_install's 402 limit_exceeded when the owner would rather not upgrade. 404 means the worker does not exist, is not on this account, or is outside this key's worker selection — including a second delete of one already gone, so a 404 on a retry means the first call worked. 409 sub_workers_outside_key = a key limited to selected workers would delete sub-workers outside its selection, so the delete is refused rather than widened; an account Admin adds them to the key first. Requires the deleteWorkers scope." +
783
806
  NEW_SCOPE_NOTE,
784
807
  auth: "manager",
785
808
  method: "delete",
@@ -790,10 +813,11 @@ const deleteWorker = {
790
813
  annotations: DELETE,
791
814
  };
792
815
  // ─── Kit install ────────────────────────────────────────────────────────────
793
- // The one place on this surface an agent can CREATE a worker: install a kit from
794
- // the public directory. Both tools need the installKits scope (keys minted
795
- // before the scope existed — including old "all" keys — lack it until an
796
- // account admin re-scopes them).
816
+ // Install a kit from the public directory — the vetted route to a new worker
817
+ // (createWorkers covers the from-scratch and clone routes). Both tools need the
818
+ // installKits scope (keys minted before the scope existed — including old "all"
819
+ // keys — lack it until an account admin re-scopes them). A key limited to
820
+ // selected workers gains the worker it installs.
797
821
  const KITS_API = "/api/manage/kits";
798
822
  const KIT_SLUG_HINT = "The kit's slug, as returned by kits_search / kit_get on the public WorkerKit Directory server. 404 not_found = no such kit (or another account's private kit — indistinguishable by design).";
799
823
  // Pinned to the backend's slug alphabet ([a-z0-9-], ≤120). Not cosmetic: encodeURIComponent
@@ -847,6 +871,7 @@ const kitInstall = {
847
871
  operatorId: z.string().uuid().optional().describe("Operator (workspace) to install into. Omit to use the account's default operator."),
848
872
  title: z.string().max(100).optional().describe("Name for the new worker (≤100 chars). Omit to use the kit's name."),
849
873
  avatar: z.string().max(40).optional().describe("Avatar slug for the new worker. Omit for the default."),
874
+ timeZoneId: z.string().trim().min(1).max(64).optional().describe("The worker's timezone, as an IANA id such as America/New_York: it decides the worker's local dates and reports, and is set before the kit's schedules are created. Ask the worker creator for it if it is not already known; never guess it from connected calendars or the server's clock. Omitted means UTC. install.effectiveTimeZoneId in the response is the zone applied."),
850
875
  slackConnectionId: z.number().int().positive().optional().describe("Action kits: pin a Slack bot from actionConnections in the install preview. No model or deployment is needed."),
851
876
  githubSelection: GITHUB_SELECTION.optional().describe("Explicit GitHub targets for the new worker. Discover them with app_github_accounts, app_github_repos and app_github_branches (discoverAppResources scope). connectionId also appears as githubAccounts[].id in kit_install_preview. Use repositoryMode 'selected' with 1-500 stable repositoryIds for a specific job. 'all' includes future repositories granted to that account: choose it only when intended. accounts:[] leaves setup for later. Required for a GitHub kit when the key has discoverAppResources; legacy keys retain the single-account fallback. Discovery and branch inputs do not grant extra access."),
852
877
  categoryChoices: z.array(z.object({
@@ -871,6 +896,7 @@ const kitInstall = {
871
896
  operatorId: params.operatorId,
872
897
  title: params.title,
873
898
  avatar: params.avatar,
899
+ timeZoneId: params.timeZoneId,
874
900
  githubSelection: params.githubSelection,
875
901
  categoryChoices: params.categoryChoices,
876
902
  inputs: params.inputs,
@@ -1072,7 +1098,7 @@ const listModels = {
1072
1098
  const listDeployments = {
1073
1099
  name: "deployments_list",
1074
1100
  title: "List Deployments",
1075
- description: "Every DEPLOYED worker on the account with its model, spend ceilings and deployment status — the fleet answer to \"what is actually able to run\". A worker that appears in workers_list but NOT here is not on the runtime, however complete it looks: that is the single most common reason a newly built worker never produces a run. Action workers (modelType none) never appear here, by design: they are not deployed, and their tools are called with worker_tool_call. Account-scoped by the key, so it takes no worker id. Requires readWorkers." +
1101
+ description: "Every DEPLOYED worker this key reaches (the whole account for an all-workers key) with its model, spend ceilings and deployment status — the fleet answer to \"what is actually able to run\". A worker that appears in workers_list but NOT here is not on the runtime, however complete it looks: that is the single most common reason a newly built worker never produces a run. Action workers (modelType none) never appear here, by design: they are not deployed, and their tools are called with worker_tool_call. Scoped by the key — its account and its worker selection — so it takes no worker id. Requires readWorkers." +
1076
1102
  RUNTIME_NOTE,
1077
1103
  auth: "manager",
1078
1104
  method: "get",
@@ -1173,7 +1199,7 @@ const undeployWorker = {
1173
1199
  const getWorkerPermissions = {
1174
1200
  name: "worker_permissions_get",
1175
1201
  title: "Get Worker Permissions",
1176
- description: "What the worker may touch, in the kit-authoring vocabulary: apps[] ({code, operations, visibleFields, allowAll, writeEnabled, providers, …} — the same shape a kit's content.apps takes), categorySlots[], contactAllowAll, blockedSenderCategories, timeframePastDays / timeframeFutureDays, maxChildTokens, and ruleCounts (per rule table, PRESENCE only — rule values never leave the worker). Read-only: permissions are never written through this surface; they change through a kit (kit_publish private, then kit_install) or in the dashboard. ruleCounts decides between the two ways to multiply a worker: worker_clone carries the rules, kit_publish from sourceWorkerId never does — so a worker with rules would install MORE permissively as a kit than it runs today. Requires the readWorkers scope.",
1202
+ description: "What the worker may touch, in the kit-authoring vocabulary: apps[] ({code, operations, visibleFields, allowAll, writeEnabled, providers, …} — the same shape a kit's content.apps takes), categorySlots[], contactAllowAll, blockedSenderCategories, timeframePastDays / timeframeFutureDays, maxChildTokens, and ruleCounts (per rule table, PRESENCE only — rule values never leave the worker). Read-only: no tool changes a worker's permissions. To add an app or an operation, tell the user what to switch on under the worker's Apps & access on the dashboard, then read this again to confirm the change. A new worker with different permissions comes from a kit instead (kit_publish private, then kit_install). ruleCounts decides between the two ways to multiply a worker: worker_clone carries the rules, kit_publish from sourceWorkerId never does — so a worker with rules would install MORE permissively as a kit than it runs today. Requires the readWorkers scope.",
1177
1203
  auth: "manager",
1178
1204
  method: "get",
1179
1205
  schema: {
@@ -1185,10 +1211,11 @@ const getWorkerPermissions = {
1185
1211
  };
1186
1212
  // ─── Kits (authoring) ───────────────────────────────────────────────────────
1187
1213
  //
1188
- // The publishKits lane. A PRIVATE kit installed with kit_install is how a worker
1189
- // is created from scratch on this surface (cloning is the only other non-kit
1190
- // path), so these descriptors teach the flow as much as the wire: read the guide
1191
- // and the vocabulary on the Directory server, validate, publish private, install.
1214
+ // The publishKits lane. A PRIVATE kit installed with kit_install is the kit route
1215
+ // to a worker from scratch (action_worker_create, decision_worker_create and
1216
+ // cloning are the createWorkers routes), so these descriptors teach the flow as
1217
+ // much as the wire: read the guide and the vocabulary on the Directory server,
1218
+ // validate, publish private, install.
1192
1219
  const KIT_REF = z.string().min(1).max(120);
1193
1220
  const KIT_REF_HINT = "The kit's slug, as returned by my_kits_list / kit_publish (its legacy numeric id is also accepted). Resolution is account-scoped: 404 not_found means no such kit on THIS account — another account's kit is indistinguishable from a nonexistent one.";
1194
1221
  const AUTHORING_FLOW_NOTE = " Authoring flow: read kit_authoring_guide (index, then schema and rules) and kit_vocabulary on the public WorkerKit Directory server first; kit_validate the exact body you will publish; publish PRIVATE (visibility 'private') and kit_install it; promote with kit_replace (visibility 'public') once a run (an Action kit: a tool call) has proved it. Requires the publishKits scope." +
@@ -1283,14 +1310,14 @@ const validateKit = {
1283
1310
  const publishKit = {
1284
1311
  name: "kit_publish",
1285
1312
  title: "Publish Kit",
1286
- description: "Publish a NEW kit on this account — from authored content, or from any worker on the account (sourceWorkerId: its instruction, texts, permissions, schedules and triggers become the kit; rule VALUES never travel, only that rules exist). Exactly one of content / sourceWorkerId. NOT idempotent: every successful call creates another listing — never retry a success; on a timeout read my_kits_list before trying again. visibility 'private' (recommended first) makes a kit only this account can install: kit_install its slug and you have a worker built from scratch through the reviewed manifest pipeline. 'public' (default) lists it in the directory after the fail-closed supply-chain scan — 503 kit_scan_unavailable means retry later with the SAME body, or publish private. 400 names the FIRST failing rule (run kit_validate first to see them all). 403 = the source worker was installed from a protected kit, whose text belongs to that kit's publisher. 409 = publisherName taken (first publish only). Caps: 20 publishes/hour per account, 100 PUBLIC kits per account (private kits are unlimited; an unlisted or admin-removed listing still holds its slot). Returns the listing (slug, status, moderationStatus, requiredInputs, lintWarnings, …); a public listing can still be flagged minutes later by the semantic scan — re-read my_kits_list, and kit_scan_get has the findings." +
1313
+ description: "Publish a NEW kit on this account — from authored content, or from any worker this key reaches (sourceWorkerId: its instruction, texts, permissions, schedules and triggers become the kit; rule VALUES never travel, only that rules exist; a worker outside the key's selection is 404 not_found). Exactly one of content / sourceWorkerId. NOT idempotent: every successful call creates another listing — never retry a success; on a timeout read my_kits_list before trying again. visibility 'private' (recommended first) makes a kit only this account can install: kit_install its slug and you have a worker built from scratch through the reviewed manifest pipeline. 'public' (default) lists it in the directory after the fail-closed supply-chain scan — 503 kit_scan_unavailable means retry later with the SAME body, or publish private. 400 names the FIRST failing rule (run kit_validate first to see them all). 403 = the source worker was installed from a protected kit, whose text belongs to that kit's publisher. 409 = publisherName taken (first publish only). Caps: 20 publishes/hour per account, 100 PUBLIC kits per account (private kits are unlimited; an unlisted or admin-removed listing still holds its slot). Returns the listing (slug, status, moderationStatus, requiredInputs, lintWarnings, …); a public listing can still be flagged minutes later by the semantic scan — re-read my_kits_list, and kit_scan_get has the findings." +
1287
1314
  AUTHORING_FLOW_NOTE,
1288
1315
  auth: "manager",
1289
1316
  method: "post",
1290
1317
  schema: {
1291
1318
  ...LISTING_SCHEMA,
1292
1319
  content: CONTENT_SCHEMA.optional(),
1293
- sourceWorkerId: z.string().uuid().optional().describe("Publish from this worker instead of content (any worker on the account — its workerId from workers_list). Never together with content."),
1320
+ sourceWorkerId: z.string().uuid().optional().describe("Publish from this worker instead of content (any worker this key reaches — its workerId from workers_list; one outside the key's selection is 404 not_found). Never together with content."),
1294
1321
  },
1295
1322
  path: KITS_API,
1296
1323
  annotations: CREATE,
@@ -1398,7 +1425,7 @@ const makeKitPrivate = {
1398
1425
  const deleteKit = {
1399
1426
  name: "kit_delete",
1400
1427
  title: "Delete Kit",
1401
- description: "Hard-delete a listing (204). PERMANENT — destroys its download history and stats; kit_unpublish is the reversible alternative. Workers installed from it are untouched. The second call is a 404. Requires the publishKits scope." +
1428
+ description: "Hard-delete a listing (204). PERMANENT; kit_unpublish is the reversible alternative. Workers installed from it are untouched. The second call is a 404. Requires the publishKits scope." +
1402
1429
  NEW_SCOPE_NOTE,
1403
1430
  auth: "manager",
1404
1431
  method: "delete",
@@ -1632,6 +1659,7 @@ const deleteMcpServer = {
1632
1659
  export const manageDescriptors = [
1633
1660
  ...appDiscoveryDescriptors,
1634
1661
  ...onboardingDescriptors,
1662
+ ...supportDescriptors,
1635
1663
  createDecisionWorker,
1636
1664
  ...actionDescriptors,
1637
1665
  setWorkerDecision,
@@ -1657,6 +1685,7 @@ export const manageDescriptors = [
1657
1685
  addMemoryItem,
1658
1686
  updateMemoryItem,
1659
1687
  deleteMemoryItem,
1688
+ setWorkerTimeZone,
1660
1689
  listSchedules,
1661
1690
  createSchedule,
1662
1691
  updateSchedule,