iit-mcp-autotask 0.10.0 → 0.11.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 +62 -18
- package/capabilities.json +5 -5
- package/dist/approvals.d.ts +112 -0
- package/dist/approvals.js +440 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +16 -1
- package/dist/doctor.js +11 -0
- package/dist/main.js +10 -0
- package/dist/mocks/fixture-ops.d.ts +8 -0
- package/dist/mocks/fixture-ops.js +33 -1
- package/dist/tools/catalog-data.d.ts +1 -1
- package/dist/tools/catalog-data.js +6 -6
- package/dist/tools/composites.js +8 -5
- package/dist/tools/dispatch.js +20 -1
- package/dist/tools/field-pointer.d.ts +10 -0
- package/dist/tools/field-pointer.js +13 -0
- package/dist/tools/lists.js +11 -6
- package/dist/tools/meta.js +77 -3
- package/dist/tools/output-file.d.ts +5 -1
- package/dist/tools/output-file.js +29 -4
- package/dist/tools/reasons.d.ts +2 -2
- package/dist/tools/reasons.js +10 -0
- package/dist/tools/resolves.js +44 -4
- package/dist/tools/schemas.d.ts +23 -0
- package/dist/tools/schemas.js +44 -7
- package/dist/tools/server.d.ts +3 -3
- package/dist/tools/server.js +9 -3
- package/dist/tools/tsv.d.ts +3 -1
- package/dist/tools/tsv.js +14 -2
- package/dist/tools/write.d.ts +6 -1
- package/dist/tools/write.js +404 -31
- package/dist/write/canonical.js +1 -1
- package/dist/write/evaluate.d.ts +9 -0
- package/dist/write/evaluate.js +8 -1
- package/dist/write/grant.d.ts +52 -0
- package/dist/write/grant.js +159 -0
- package/dist/write/hard-rules.d.ts +7 -1
- package/dist/write/hard-rules.js +9 -1
- package/dist/write/manual.d.ts +28 -0
- package/dist/write/manual.js +54 -0
- package/dist/write/persisted.d.ts +26 -0
- package/dist/write/persisted.js +63 -0
- package/dist/write/policy-file.d.ts +58 -0
- package/dist/write/policy-file.js +124 -5
- package/dist/write/runtime.js +2 -0
- package/dist/write/store.d.ts +53 -2
- package/dist/write/store.js +99 -19
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/company-360.md +2 -2
- package/skills/contract-profitability.md +2 -2
- package/skills/filter-semantics.md +5 -4
- package/skills/invoice-export.md +1 -1
- package/skills/log-time-entry.md +4 -4
- package/skills/slim-reads.md +2 -2
- package/skills/ticket-triage.md +1 -1
- package/skills/unbilled-work-review.md +3 -3
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ context into one call, a generic read layer for the long tail, one result envelo
|
|
|
8
8
|
packaged playbooks, strong security boundaries, and predictable per-tenant in-process state (pooled
|
|
9
9
|
client + SDK-owned TTL caches).
|
|
10
10
|
|
|
11
|
-
> **Package status (2026-10-
|
|
12
|
-
> at `0.
|
|
11
|
+
> **Package status (2026-10-22):** the runtime dependency `node-autotask` is published to public npm
|
|
12
|
+
> at `0.23.0`; the committed `pnpm-lock.yaml` pins that
|
|
13
13
|
> release with its integrity hash, so installs resolve deterministically. The dependency is always npm
|
|
14
|
-
> (node-autotask pinned exactly at `0.
|
|
14
|
+
> (node-autotask pinned exactly at `0.23.0`) — never a `file:`, `git:`, `link:` or workspace reference
|
|
15
15
|
> to a local checkout.
|
|
16
16
|
|
|
17
17
|
> **0.6.0 is a breaking release** (unreleased): tool names, result shapes and configuration all
|
|
@@ -21,7 +21,7 @@ client + SDK-owned TTL caches).
|
|
|
21
21
|
|
|
22
22
|
The **core** toolset (10 tools) is always served; `AUTOTASK_TOOLSETS` adds `service`, `records` and
|
|
23
23
|
`finance` (or `all`, 18 tools); the operator profile adds two. The whole tool surface (20 tools) is
|
|
24
|
-
listed below. Every other operation in the SDK's 1,
|
|
24
|
+
listed below. Every other operation in the SDK's 1,216-operation registry stays
|
|
25
25
|
reachable through the generic layer: `autotask_catalog` finds the key, `autotask_describe` serves
|
|
26
26
|
its contract, `autotask_read` runs it.
|
|
27
27
|
|
|
@@ -35,13 +35,18 @@ its contract, `autotask_read` runs it.
|
|
|
35
35
|
|
|
36
36
|
- `autotask_resolve` turns any identifier into a record: an id or name, `by: "ticket_number"`,
|
|
37
37
|
`"serial"` or `"email"`, in one `resource` or swept across the five workflow resources;
|
|
38
|
-
`match: "contains"` for companies or a sweep. Every match carries `ref`, its human reference.
|
|
38
|
+
`match: "contains"` for companies or a sweep. Every match carries `ref`, its human reference. A sweep returns compact matches (`resource`, `ref`, `id`, the human fields) unless `expand: true` (the full record under `record`); `limit` caps the TOTAL matches across resources (resource order, then the SDK's order) and a contains sweep reports `truncated` and per-resource `counts {returned, found}`.
|
|
39
39
|
- `autotask_query` is the bounded filtered list over `resource`: `tickets`, `time_entries`,
|
|
40
40
|
`service_calls`, `contracts`, `time_off_requests` (limit default 50, max 2500).
|
|
41
41
|
- `autotask_read` runs any read by exact key, or up to 10 at once (`items`). Unbilled work
|
|
42
42
|
(`companies.unbilledWork`), contract profitability (`contracts.profitability`), aggregates
|
|
43
43
|
(`operations.aggregate`) and SLA health (`tickets.slaHealthCheck`) run here; `autotask_guide`
|
|
44
|
-
holds their recipes.
|
|
44
|
+
holds their recipes. `autotask_read` and `autotask_fetch_many` answer ONE envelope (the next minor,
|
|
45
|
+
breaking): `data = { results: Slot[], ok, failed, budget?, budgetWarning? }`, an array in input
|
|
46
|
+
order even for a single read (`data.results[0].result`). A slot is `{ ok: true, operation, result,
|
|
47
|
+
windowNote? }` or `{ ok: false, operation, error }`; fetch_many slots also echo `resource` and `id`
|
|
48
|
+
(`operation` is `<resource>.get`; a miss is `ok: false` with `NOT_FOUND`). A single read that fails
|
|
49
|
+
still answers the whole-call error envelope.
|
|
45
50
|
- `autotask_guide` serves the packaged playbooks — the server's skill layer (below).
|
|
46
51
|
|
|
47
52
|
### `autotask_guide`: the packaged playbooks (the server's skill layer)
|
|
@@ -65,7 +70,7 @@ The playbooks are discoverable in both directions:
|
|
|
65
70
|
`autotask_describe` for an operation answers with the `playbooks[]` that use it, so a model
|
|
66
71
|
that lands on an operation is pointed at the recipe.
|
|
67
72
|
|
|
68
|
-
Composites bound **each part independently** (every part reads its own declared limit argument); a
|
|
73
|
+
Composites bound **each part independently** (every part reads its own declared limit argument; `autotask_company_full_profile` defaults its contact, note, open-ticket and project parts to 10 rows each, `capped: true` says more exist; `fields` projects columns per part); a
|
|
69
74
|
part miss never fails the call (typed `absent`); only a missing root record fails the composite.
|
|
70
75
|
|
|
71
76
|
**Every result is one envelope** (`structuredContent`, mirrored as JSON in the first text block):
|
|
@@ -141,11 +146,46 @@ What the policy can and cannot do:
|
|
|
141
146
|
`forbidRemovals`, `requireReason`, UTC `windows`, `duplicateCheck` (required for creates) and
|
|
142
147
|
`allowUnattended`. Globally: `write.scope.companyIds`, `createdHereWithinDays`, and
|
|
143
148
|
`limits.writesPerHour`/`writesPerDay`.
|
|
149
|
+
- Moving a time entry or ticket charge to another ticket (#107): list `time_entries.moveToTicket` or
|
|
150
|
+
`ticket_charges.moveToTicket` with an `owner` block, e.g. `{"approval": "none", "allowUnattended": true,
|
|
151
|
+
"owner": {"mode": "createdHere", "days": 7}}` (`createdHere`: this server created the source for this
|
|
152
|
+
principal within 1-14 days; or `{"mode": "resourceIDs", "ids": [..]}` for time entries, whose `resourceID`
|
|
153
|
+
must be listed; a charge has no owning resource). Preview `autotask_write {operation, args: [sourceId,
|
|
154
|
+
toTicketId]}`, then apply. The server creates the copy on the target ticket, verifies it and deletes the
|
|
155
|
+
original (the vendor ignores an update of `ticketID`): the new record gets a new id, create date and
|
|
156
|
+
creator, and notifications may fire. A move into another company or contract always needs a signed
|
|
157
|
+
approval grant; an unfinished earlier move of the same source blocks another until its ledger entry ages
|
|
158
|
+
out (14 days). Billed, attached, approved, closed-target and older-than-14-days sources are refused.
|
|
144
159
|
- Hard rules no policy overrides: `authenticate.*` is never served; operations the SDK flags
|
|
145
160
|
`requiresApproval`, and non-read sensitive ones (webhooks, `resources.transferOwnership`), always
|
|
146
|
-
need approval; destructive and account-wide operations need it
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
need approval; destructive and account-wide operations need it, and `allowUnattended` cannot waive
|
|
162
|
+
that for an operation the hard rules cover. In approval mode `policy` no approver
|
|
163
|
+
exists: an approval-required write answers `needs_approval` with `next: manual_action` (no plan
|
|
164
|
+
handle) and is never executed. Approval mode `external` (`approvalMode: "external"` plus an `approvals`
|
|
165
|
+
block listing ed25519 approver keys, each scoped to operations) is being added in stages (#106): a gated
|
|
166
|
+
preview stores a pending plan (`next: await_approval`, handle, hash, expiry of grant window + 5 minutes) that
|
|
167
|
+
executes only with a valid ed25519 grant file `approvals/grants/<handle>.json` signed by a listed approver
|
|
168
|
+
(apply verifies it; deny rules still win; one use per plan). A human decides with the CLI, run in their own
|
|
169
|
+
terminal as the server's OS user with `AUTOTASK_STATE_DIR` and `AUTOTASK_POLICY_FILE` set as the server has them:
|
|
170
|
+
`autotask-mcp keygen <approver-id> --ops <op,op|*>` (creates a passphrase-sealed ed25519 key, mode 0600, default
|
|
171
|
+
`~/.config/iit-mcp-autotask/approver.key` or `$AUTOTASK_APPROVER_KEY_FILE`, and prints the entry for
|
|
172
|
+
`approvals.approvers`), then `autotask-mcp approvals list|show|approve|reject <handle>`. `show` and `approve`
|
|
173
|
+
recompute the plan hash and snapshot hash from the stored plan and refuse on a mismatch or a changed policy; they
|
|
174
|
+
print only verified fields, and every agent-supplied string (arguments, the agent's reason) is JSON-encoded behind
|
|
175
|
+
`| ` inside a labelled untrusted block with control and bidi characters stripped. `approve` and `reject` need you to
|
|
176
|
+
type the first 8 characters of the plan hash and the key passphrase; the passphrase is read from `/dev/tty` only
|
|
177
|
+
(never the environment, arguments, a pipe or a file; there is no `--yes`), so the command refuses without a
|
|
178
|
+
terminal and on Windows. `approve` and `reject` refuse while any shown value was cut for display (it would be signed in full): review with `show <handle> --full`, then pass `--full`. The first decision per plan is final. This stops accidents, not an agent that can drive a
|
|
179
|
+
pty, and on stdio the sealed key file can be attacked offline by anything running as the same user. On HTTP run it
|
|
180
|
+
where the state dir lives (for example `docker exec -it` as the server's user). On stdio this is a
|
|
181
|
+
guardrail against a mistaken agent, not a boundary against an adversarial agent running as the same OS
|
|
182
|
+
user; it is a real boundary only on HTTP with the server, its key material and the policy file isolated
|
|
183
|
+
from the agent. Any edit to the policy file (including `approvals`) changes the policy hash and voids
|
|
184
|
+
every outstanding plan. `data.manual` carries a fingerprint of the record (ids, ticket,
|
|
185
|
+
start time, hours, resource — numbers and instants only, never vendor text) and, when the pinned
|
|
186
|
+
SDK provides `operations.uiLink`, a clickable Autotask deep link; a person finishes the job in
|
|
187
|
+
Autotask. A policy that sets `approval: "none"` on such an operation changes nothing: boot logs a
|
|
188
|
+
warning and `doctor` prints it. There is no `autotask_delete` (D-12): deletes preview through `autotask_write`.
|
|
149
189
|
- The company scope is read from the live records the write touches (the target, its parent, the
|
|
150
190
|
ticket, project, task or contract it references, and any `companyID` it sets), never from names
|
|
151
191
|
or titles. A reference that cannot be read is refused.
|
|
@@ -162,7 +202,7 @@ Outcomes the caller acts on: `ok`/`proceed`; `needs_input`/`supply_input` (`miss
|
|
|
162
202
|
field); `needs_approval`/`await_approval`; `denied`/`stop`; `STATE_DRIFT` or `PLAN_EXPIRED` →
|
|
163
203
|
`repreview`; a failed write: `WRITE_FAILED` with `stop` (a deterministic rejection; it replays,
|
|
164
204
|
fix the input and use a new key) or `RATE_LIMITED` with `retry` (nothing was written: apply the
|
|
165
|
-
same plan again after the wait); an unknown outcome (`WRITE_OUTCOME_UNKNOWN`,
|
|
205
|
+
same plan again after the wait); a vendor that accepted an update but kept the old value (`FIELD_NOT_PERSISTED` with `stop`: the apply reads the stored record back and compares every field you sent; the write itself is `succeeded`, `data.record` shows what is stored and `data.notPersisted` names the fields — do not repeat the update, the vendor does not support changing them; a replay answers the same); an unknown outcome (`WRITE_OUTCOME_UNKNOWN`,
|
|
166
206
|
`verify_then_repreview`): that plan is never sent again, and re-applying it or re-previewing under
|
|
167
207
|
the same key only replays `unknown`. Re-read the target; if the write is absent, preview again under
|
|
168
208
|
a new `idempotency_key` (a new attempt) after the 10-minute cooldown; its duplicate query refuses
|
|
@@ -262,9 +302,13 @@ Three per-call options change the result form:
|
|
|
262
302
|
| `untrusted_wrapper: false` | skips the `<untrusted_content>` wrapping for that call only; the result then carries `untrustedWrapper: 'disabled'` and the human text says so. Absent (the default) wraps exactly as before. |
|
|
263
303
|
| `output_path: '<relative path>'` | **stdio transport only**: walks the FULL match set with the resource's own query iterator, writes the envelope JSON to a file under `AUTOTASK_OUTPUT_DIR`, and returns a summary (`outputPath`, `columns`, `rowCount`, the first rows, `bytes`) instead of the table. |
|
|
264
304
|
|
|
265
|
-
`output_path` is deliberately strict: the deployment
|
|
266
|
-
that already exists and is not the filesystem root (
|
|
267
|
-
the server never creates
|
|
305
|
+
`output_path` is deliberately strict: the deployment sets an **absolute** `AUTOTASK_OUTPUT_DIR`
|
|
306
|
+
that already exists and is not the filesystem root (a configured root that is missing is refused —
|
|
307
|
+
the server never creates it). When it is unset the root defaults to `output/` under `AUTOTASK_STATE_DIR`
|
|
308
|
+
(the existing, operator-configured state dir; the server creates that one subdirectory and nothing
|
|
309
|
+
above it; with neither variable set the feature refuses). `format: 'tsv'` with `output_path` writes the
|
|
310
|
+
TSV table to the file (the result carries the path, never the table); `latest` and `note_chars` still
|
|
311
|
+
cannot combine with it. And the value must be a path **relative** to that root that names
|
|
268
312
|
a file (a trailing separator, an absolute path, a NUL byte, and a target that is an existing
|
|
269
313
|
directory are all refused). **Nothing outside the root is ever created,
|
|
270
314
|
file or directory**: the deepest existing ancestor of the target is canonicalised (`realpath`) and
|
|
@@ -480,7 +524,7 @@ non-zero on an invalid config (fail-fast) instead of failing at the first tool c
|
|
|
480
524
|
| `AUTOTASK_CACHE_PRESET` | no | `on` | per-tenant in-process caches (see [Per-tenant pool & cache](#per-tenant-pool--cache)): `on` (default) sends no cache blocks — the SDK's default-on caches apply to each pooled tenant client (read ~5 min + the plan-derived reference table, field ~5 min, resource-name ~1 h); `off` opts every SDK cache channel out explicitly (every read re-dials the vendor) |
|
|
481
525
|
| `AUTOTASK_COMPOSITE_NAMES` | no | `off` | opt-in name/label siblings on the four SDK-backed composites (`tickets.fullContext`, `companies.fullProfile`, `configuration_items.profile`, `projects.getContext`) — reuses the SDK's existing caches (no new cache); `off` = byte-identical output to today; `on` adds the name siblings + the top-level `names` section, bounded by the SDK name-read budget (default 20 cold wire reads, lowerable only) |
|
|
482
526
|
| `AUTOTASK_FIELD_CACHE_TTL_MS` | no | unset (SDK 5 min) | pass-through to the SDK's field-metadata cache TTL (positive integer ms), sent as the scalar `fieldCache` block on the `on` preset path only — the server holds no TTL value of its own: unset or invalid (non-integer, <= 0) sends no `fieldCache` block and the SDK default (5 min) applies; `off` keeps its explicit opt-out; `fieldCatalog` `fresh: true` still bypasses the cache |
|
|
483
|
-
| `AUTOTASK_OUTPUT_DIR` | no | unset | the ONE directory the `output_path` option may write into (the bounded query tools and the invoice export; stdio transport only). Unset = the feature refuses; must be absolute, must already EXIST (the server never creates the root) and must not be the filesystem root. A caller path is relative to this root, and the root and target are canonically resolved (`realpath`) so a traversal or symlink escape is refused — with nothing created outside the root. See [Query results](#query-results-tsv-output_path-and-the-saved-file-shape) |
|
|
527
|
+
| `AUTOTASK_OUTPUT_DIR` | no | unset | the ONE directory the `output_path` option may write into (the bounded query tools and the invoice export; stdio transport only). Unset = defaults to `output/` under `AUTOTASK_STATE_DIR` (the feature refuses when neither is set); must be absolute, must already EXIST (the server never creates the root) and must not be the filesystem root. A caller path is relative to this root, and the root and target are canonically resolved (`realpath`) so a traversal or symlink escape is refused — with nothing created outside the root. See [Query results](#query-results-tsv-output_path-and-the-saved-file-shape) |
|
|
484
528
|
| `AUTOTASK_RATE_LIMIT_PER_HOUR` | no | 8000 (agent profile) | override the SDK's local hourly budget (never above the vendor cap; `null`/off is rejected at boot — it would void the budget guard) |
|
|
485
529
|
| `AUTOTASK_MAX_CONCURRENT` | no | 3 (agent profile) | override the SDK's local concurrency bound |
|
|
486
530
|
| `AUTOTASK_TIMEOUT_MS` | no | 20000 (agent profile) | whole-call budget |
|
|
@@ -719,7 +763,7 @@ independent review with its resolutions, [`docs/reviews/0.6.0.md`](docs/reviews/
|
|
|
719
763
|
|
|
720
764
|
## SDK dependency
|
|
721
765
|
|
|
722
|
-
`iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.
|
|
766
|
+
`iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.23.0`** (an exact pin —
|
|
723
767
|
no range operator; subpaths `.` / `/capabilities` / `/operations` / `/resources` / `/types` /
|
|
724
768
|
`/pool`) — the only **vendor** dependency; the other two runtime dependencies are
|
|
725
769
|
`@modelcontextprotocol/server` (the MCP framework) and `zod` (schema validation). It never
|
|
@@ -729,7 +773,7 @@ user — the published tarball ships no lockfile) must resolve to the release th
|
|
|
729
773
|
reviewed against, or the plan-hash boot check refuses to start.
|
|
730
774
|
|
|
731
775
|
**Publishing `node-autotask` is the maintainer's action, done manually.** It is published on the
|
|
732
|
-
public registry at `0.
|
|
776
|
+
public registry at `0.23.0` (published 2026-10-21); the
|
|
733
777
|
exact pin and the committed `pnpm-lock.yaml` (with `pnpm-workspace.yaml`) pin the real published
|
|
734
778
|
tarball with its integrity hash, so installs resolve to exactly that release — the lockfile
|
|
735
779
|
freezes the pnpm/git/Docker paths, the pin freezes the npm/npx paths. The docs are written against this
|
|
@@ -823,7 +867,7 @@ physical assets (Autotask models them as Configuration Items — covered).
|
|
|
823
867
|
|
|
824
868
|
| # | Item | Status |
|
|
825
869
|
|---|------|--------|
|
|
826
|
-
| B1 | **Project phase & task lists** (W7 sub-capability): no `project_tasks`/`project_phases` operations in the SDK's 1,
|
|
870
|
+
| B1 | **Project phase & task lists** (W7 sub-capability): no `project_tasks`/`project_phases` operations in the SDK's 1,216-op registry. W7 ships with project status + context; phase/task lists excluded and documented. Classification (SDK gap vs vendor API-version limit) is decided by recon's spec-side check (A2). | blocked — documented |
|
|
827
871
|
| B2 | **Time-entry writes on ww29/australia-nz for API-only users**: vendor 500 (live-verified). Time-entry creation works where the API user is a real resource, through the operator profile (`log-time-entry`); on ww29 the write is classified `failed` (a 500 with a vendor `errors[]` body), never `unknown`. | zone limitation |
|
|
828
872
|
| B3 | **`time_entries.getPosted`/`getUnposted` 500 on ww29** (no posted-state filter on the zone's entity). Reachable via the generic layer; `postedWithin` is the zone-robust alternative. | zone limitation |
|
|
829
873
|
| B4 | **CI create on tenants without products** (ww29 product wall: `products.count = 0`): `configuration_items.create` requires an existing `productID`. No dedicated CI-create in v1; the wall is documented on the generic side. | zone limitation |
|
package/capabilities.json
CHANGED
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
"id": "autotask.platform.catalog",
|
|
73
|
-
"description": "Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,
|
|
73
|
+
"description": "Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,216 registry operations, with effect and whether this deployment serves it. No vendor call.",
|
|
74
74
|
"mcpPrimitive": "tool",
|
|
75
75
|
"toolName": "autotask_catalog",
|
|
76
76
|
"type": "read",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
},
|
|
119
119
|
{
|
|
120
120
|
"id": "autotask.discovery.resolve",
|
|
121
|
-
"description": "Find the record behind an identifier: id or name, ticket number, configuration item serial number, contact email (by), in one resource or swept across several. Exact by default; contains for companies or a sweep
|
|
121
|
+
"description": "Find the record behind an identifier: id or name, ticket number, configuration item serial number, contact email (by), in one resource or swept across several. Exact by default; contains for companies or a sweep (compact; limit = total).",
|
|
122
122
|
"mcpPrimitive": "tool",
|
|
123
123
|
"toolName": "autotask_resolve",
|
|
124
124
|
"type": "read",
|
|
@@ -150,7 +150,7 @@
|
|
|
150
150
|
},
|
|
151
151
|
{
|
|
152
152
|
"id": "autotask.discovery.fetch_many",
|
|
153
|
-
"description": "Fetch up to 500 records by numeric id across mixed resources in one call: items: [{resource, id, fields?}].
|
|
153
|
+
"description": "Fetch up to 500 records by numeric id across mixed resources in one call: items: [{resource, id, fields?}]. Answers data.results[{ok, operation, result|error}], ok, failed (same as autotask_read); a miss is a typed NOT_FOUND slot.",
|
|
154
154
|
"mcpPrimitive": "tool",
|
|
155
155
|
"toolName": "autotask_fetch_many",
|
|
156
156
|
"type": "read",
|
|
@@ -198,7 +198,7 @@
|
|
|
198
198
|
},
|
|
199
199
|
{
|
|
200
200
|
"id": "autotask.company.full_profile",
|
|
201
|
-
"description": "One company with locations, contacts, notes, open tickets and projects
|
|
201
|
+
"description": "One company with locations, contacts, notes, open tickets and projects, 10 rows per part (capped: true = more exist; raise the limit). fields {part:[names]} trims columns. Open = status not 5. Missing parts degrade to typed absent.",
|
|
202
202
|
"mcpPrimitive": "tool",
|
|
203
203
|
"toolName": "autotask_company_full_profile",
|
|
204
204
|
"type": "read",
|
|
@@ -380,7 +380,7 @@
|
|
|
380
380
|
"autotask_describe",
|
|
381
381
|
"autotask_read"
|
|
382
382
|
],
|
|
383
|
-
"backingRegistryOperations":
|
|
383
|
+
"backingRegistryOperations": 1216,
|
|
384
384
|
"note": "autotask_catalog finds the exact operation key (ranked search), autotask_describe serves its contract, autotask_read runs any effect=read key. Writes go through the operator profile (autotask_write then autotask_apply); authenticate.* is never served."
|
|
385
385
|
},
|
|
386
386
|
"schemas": {}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* approvals.ts — the `autotask-mcp approvals` and `keygen` subcommands (issue #106, approvalMode external).
|
|
3
|
+
*
|
|
4
|
+
* A human runs these in their own terminal as the server's OS user. They never start a server and never
|
|
5
|
+
* call `openStateStore` (it registers a holder, takes locks, rewrites the ledger and prunes plans): they
|
|
6
|
+
* read `state.key`, decrypt the pending plan, and write ONE file, `approvals/grants/<handle>.json`, with
|
|
7
|
+
* exclusive create so the first decision is final. The server audits the grant when it consumes it.
|
|
8
|
+
*
|
|
9
|
+
* What is shown is re-derived, never stored text: the plan hash and snapshot hash are recomputed from the
|
|
10
|
+
* stored fields and the command refuses on a mismatch or when the policy changed. Every agent- or
|
|
11
|
+
* vendor-sourced string is stripped of control/bidi characters, JSON-encoded, stripped of control/bidi characters, capped, and printed behind a
|
|
12
|
+
* `|` prefix inside a block labelled untrusted, so it cannot pose as a verified line.
|
|
13
|
+
*
|
|
14
|
+
* Guardrails, not a boundary: the passphrase is read from /dev/tty only (no env, argv, stdin, flag or file),
|
|
15
|
+
* which stops accidents, not an agent that drives a pty. On stdio the encrypted key file can be brute-forced
|
|
16
|
+
* offline by anything running as the same user.
|
|
17
|
+
*/
|
|
18
|
+
import { type KeyObject } from 'node:crypto';
|
|
19
|
+
import { type LoadedPolicy } from './write/policy-file.js';
|
|
20
|
+
import { type PlanRecord } from './write/store.js';
|
|
21
|
+
/** scrypt cost floor for the approver key file: N = 2^17, r = 8 (Node's default maxmem refuses this, so it is set). */
|
|
22
|
+
export declare const SCRYPT_N: number;
|
|
23
|
+
export declare const SCRYPT_R = 8;
|
|
24
|
+
/** How the commands talk to the human: only the real implementation touches /dev/tty. */
|
|
25
|
+
export interface Prompter {
|
|
26
|
+
/** Show text to the human. */
|
|
27
|
+
say(text: string): void;
|
|
28
|
+
/** Read one line with echo. */
|
|
29
|
+
ask(question: string): string;
|
|
30
|
+
/** Read one line without echo (a passphrase). */
|
|
31
|
+
secret(question: string): string;
|
|
32
|
+
}
|
|
33
|
+
export declare class ApprovalsError extends Error {
|
|
34
|
+
}
|
|
35
|
+
/** The controlling terminal, or an error: no tty means no prompt, and the command exits non-zero. */
|
|
36
|
+
export declare function ttyPrompter(): Prompter & {
|
|
37
|
+
close(): void;
|
|
38
|
+
};
|
|
39
|
+
interface KeyFile {
|
|
40
|
+
v: 1;
|
|
41
|
+
/** `ed25519:` + base64url of the raw public key (what goes in the policy file). */
|
|
42
|
+
publicKey: string;
|
|
43
|
+
kdf: {
|
|
44
|
+
N: number;
|
|
45
|
+
r: number;
|
|
46
|
+
p: 1;
|
|
47
|
+
salt: string;
|
|
48
|
+
};
|
|
49
|
+
iv: string;
|
|
50
|
+
tag: string;
|
|
51
|
+
ct: string;
|
|
52
|
+
}
|
|
53
|
+
/** The key file's default path: `AUTOTASK_APPROVER_KEY_FILE`, else `~/.config/iit-mcp-autotask/approver.key`. */
|
|
54
|
+
export declare const keyFilePath: (env: Record<string, string | undefined>) => string;
|
|
55
|
+
export declare function sealKey(privateKey: KeyObject, publicKey: string, passphrase: string): KeyFile;
|
|
56
|
+
/** Decrypt a key file; refuses weak kdf parameters (the file is attacker-writable on stdio) and a wrong passphrase. */
|
|
57
|
+
export declare function openKey(file: KeyFile, passphrase: string): KeyObject;
|
|
58
|
+
/**
|
|
59
|
+
* An untrusted value for display: every string (and key) stripped of control/bidi characters, then the value JSON-encoded
|
|
60
|
+
* ONCE, so a string stays quoted while null, numbers, booleans, arrays and objects print as themselves (`null` and
|
|
61
|
+
* `"null"` are different writes and must look different). Capped; a cut value is counted in `cut`.
|
|
62
|
+
*/
|
|
63
|
+
export declare const untrusted: (value: unknown, cap?: number, cut?: {
|
|
64
|
+
n: number;
|
|
65
|
+
}) => string;
|
|
66
|
+
/** The plan's state dir files, and a pending plan checked against every hash that can be recomputed. */
|
|
67
|
+
export interface Verified {
|
|
68
|
+
plan: PlanRecord;
|
|
69
|
+
loaded: LoadedPolicy;
|
|
70
|
+
}
|
|
71
|
+
export interface Env {
|
|
72
|
+
stateDir: string;
|
|
73
|
+
policyFile: string;
|
|
74
|
+
}
|
|
75
|
+
export declare function envFrom(env: Record<string, string | undefined>): Env;
|
|
76
|
+
/** Load the plan and refuse unless every recomputable hash matches and the policy is the one it was previewed under. */
|
|
77
|
+
export declare function verifyPlan(env: Env, handle: string, now: number): Verified;
|
|
78
|
+
/** One line saying what is being decided: repeated at the confirmation prompt so it is the last thing read. */
|
|
79
|
+
export declare const summaryLine: (plan: PlanRecord) => string;
|
|
80
|
+
/**
|
|
81
|
+
* The hash-verified description of a plan. Order: verified header, the agent's reason, the arguments (signed content:
|
|
82
|
+
* a value cut for display counts in `truncated` and blocks signing), then the records as they stand (context).
|
|
83
|
+
* Untrusted strings are stripped, JSON-encoded and printed behind `| `.
|
|
84
|
+
*/
|
|
85
|
+
export declare function describePlan(v: Verified, full?: boolean): {
|
|
86
|
+
lines: string[];
|
|
87
|
+
truncated: number;
|
|
88
|
+
};
|
|
89
|
+
export declare function listPending(env: Env, now: number): string[];
|
|
90
|
+
/** Sign and write a decision. Exclusive create: the first decision for a handle is final. */
|
|
91
|
+
export declare function decide(opts: {
|
|
92
|
+
env: Env;
|
|
93
|
+
handle: string;
|
|
94
|
+
decision: 'approve' | 'reject';
|
|
95
|
+
prompt: Prompter;
|
|
96
|
+
keyFile: string;
|
|
97
|
+
now: () => number;
|
|
98
|
+
full?: boolean;
|
|
99
|
+
}): string;
|
|
100
|
+
/** Generate an approver key, seal it under a passphrase, and return the policy entry to paste into `approvals.approvers`. */
|
|
101
|
+
export declare function keygen(opts: {
|
|
102
|
+
id: string;
|
|
103
|
+
ops: string[];
|
|
104
|
+
prompt: Prompter;
|
|
105
|
+
keyFile: string;
|
|
106
|
+
}): string;
|
|
107
|
+
export declare const APPROVALS_USAGE = "autotask-mcp approvals list\nautotask-mcp approvals show <handle> [--full]\nautotask-mcp approvals approve <handle> [--full]\nautotask-mcp approvals reject <handle> [--full]\nautotask-mcp keygen <approver-id> --ops <op,op|*>\n\nRun these yourself, in a terminal, as the server's OS user, with AUTOTASK_STATE_DIR and AUTOTASK_POLICY_FILE\nset as the server has them. The passphrase is read from /dev/tty only. Key file: $AUTOTASK_APPROVER_KEY_FILE,\nelse ~/.config/iit-mcp-autotask/approver.key.";
|
|
108
|
+
/** Run a subcommand; returns the process exit code. `argv` is everything after `autotask-mcp`. */
|
|
109
|
+
export declare function runApprovals(argv: string[], env: Record<string, string | undefined>, out: (t: string) => void, prompter?: () => Prompter & {
|
|
110
|
+
close(): void;
|
|
111
|
+
}, now?: () => number): number;
|
|
112
|
+
export {};
|