iit-mcp-autotask 0.15.0 → 0.26.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/.env.example +9 -5
- package/README.md +49 -68
- package/capabilities.json +2 -4
- package/dist/access/gate.d.ts +29 -0
- package/dist/access/gate.js +75 -0
- package/dist/access/write-env.d.ts +27 -0
- package/dist/access/write-env.js +161 -0
- package/dist/auth.d.ts +13 -0
- package/dist/auth.js +19 -0
- package/dist/config/constants.d.ts +2 -2
- package/dist/config/constants.js +7 -2
- package/dist/config/load.js +1 -1
- package/dist/config/parse.d.ts +1 -1
- package/dist/config/parse.js +1 -1
- package/dist/config/schema.d.ts +8 -4
- package/dist/config/schema.js +7 -23
- package/dist/config.d.ts +0 -1
- package/dist/config.js +0 -1
- package/dist/doctor.d.ts +29 -1
- package/dist/doctor.js +6 -29
- package/dist/http.d.ts +1 -1
- package/dist/http.js +15 -4
- package/dist/main.js +10 -11
- package/dist/mocks/harness.js +1 -1
- package/dist/moves.d.ts +1 -1
- package/dist/moves.js +3 -3
- package/dist/policy.d.ts +1 -1
- package/dist/policy.js +2 -2
- package/dist/tools/capabilities-manifest.js +3 -3
- package/dist/tools/catalog-data.d.ts +1 -1
- package/dist/tools/catalog-data.js +1 -1
- package/dist/tools/context/derive.js +1 -1
- package/dist/tools/context/types.d.ts +5 -0
- package/dist/tools/dispatch.d.ts +2 -1
- package/dist/tools/dispatch.js +13 -7
- package/dist/tools/meta.js +1 -1
- package/dist/tools/reasons.d.ts +3 -3
- package/dist/tools/reasons.js +3 -12
- package/dist/tools/register.js +10 -4
- package/dist/tools/schemas.d.ts +6 -15
- package/dist/tools/server-version.d.ts +1 -1
- package/dist/tools/server-version.js +1 -1
- package/dist/tools/server.d.ts +4 -4
- package/dist/tools/server.js +25 -14
- package/dist/tools/surface.d.ts +6 -2
- package/dist/tools/surface.js +45 -0
- package/dist/tools/write/apply-phases.d.ts +18 -13
- package/dist/tools/write/apply-phases.js +43 -75
- package/dist/tools/write/apply.js +32 -9
- package/dist/tools/write/facts.d.ts +7 -21
- package/dist/tools/write/facts.js +7 -30
- package/dist/tools/write/outcome.d.ts +3 -8
- package/dist/tools/write/outcome.js +1 -19
- package/dist/tools/write/payload.d.ts +1 -1
- package/dist/tools/write/payload.js +1 -1
- package/dist/tools/write/preview.js +26 -53
- package/dist/tools/write/snapshot.d.ts +0 -1
- package/dist/tools/write/snapshot.js +2 -3
- package/dist/tools/write.d.ts +1 -1
- package/dist/tools/write.js +1 -1
- package/dist/tty.d.ts +13 -0
- package/dist/tty.js +44 -0
- package/dist/write/canonical.d.ts +6 -0
- package/dist/write/canonical.js +4 -0
- package/dist/write/evaluate.d.ts +13 -37
- package/dist/write/evaluate.js +21 -89
- package/dist/write/hard-rules.d.ts +12 -14
- package/dist/write/hard-rules.js +2 -4
- package/dist/write/runtime.d.ts +9 -6
- package/dist/write/runtime.js +22 -14
- package/dist/write/store/constants.d.ts +1 -1
- package/dist/write/store/constants.js +1 -1
- package/dist/write/store/ledger.d.ts +1 -1
- package/dist/write/store/ledger.js +1 -14
- package/dist/write/store/plan-store.d.ts +3 -6
- package/dist/write/store/plan-store.js +3 -31
- package/dist/write/store/plans.d.ts +1 -1
- package/dist/write/store/plans.js +1 -1
- package/dist/write/store/types.d.ts +5 -34
- package/dist/write/store.d.ts +1 -1
- package/dist/write/store.js +1 -3
- package/examples/agentgateway.write-gate.yaml +29 -0
- package/package.json +3 -3
- package/server.json +4 -6
- package/skills/log-time-entry.md +5 -5
- package/dist/approvals.d.ts +0 -112
- package/dist/approvals.js +0 -440
- package/dist/write/grant.d.ts +0 -52
- package/dist/write/grant.js +0 -159
- package/dist/write/manual.d.ts +0 -28
- package/dist/write/manual.js +0 -54
- package/dist/write/policy-file.d.ts +0 -136
- package/dist/write/policy-file.js +0 -231
- package/examples/policy.time-entries.json +0 -47
package/.env.example
CHANGED
|
@@ -50,14 +50,18 @@ MCP_TRANSPORT=stdio
|
|
|
50
50
|
|
|
51
51
|
# --- Profile and toolsets (0.6.0) ---
|
|
52
52
|
# readonly (default): reads only, no state. operator: adds autotask_write (preview) and
|
|
53
|
-
# autotask_apply (execute the previewed plan), governed by
|
|
53
|
+
# autotask_apply (execute the previewed plan), governed by AUTOTASK_WRITE; needs the state dir and run scope below.
|
|
54
54
|
AUTOTASK_PROFILE=readonly
|
|
55
|
-
# operator:
|
|
56
|
-
#
|
|
55
|
+
# operator: which writes are served: allow, gate (a person confirms) or deny per operation; unlisted is read-only.
|
|
56
|
+
# Grammar, dedup, limits and trusted host prompts: docs/write-gate.md.
|
|
57
|
+
# AUTOTASK_WRITE=*:gate,tickets.*:allow,time_entries.create:allow,companies.update:deny
|
|
58
|
+
# AUTOTASK_DEDUP=companies.create:companyName
|
|
59
|
+
# AUTOTASK_WRITE_LIMITS=50/h,200/d
|
|
60
|
+
# AUTOTASK_TRUSTED_HOST_PROMPT=stdio
|
|
57
61
|
# operator: the state dir (mode 0700; plans, ledger, audit log, lock; one process per dir).
|
|
58
62
|
# AUTOTASK_STATE_DIR=/var/lib/autotask-mcp
|
|
59
|
-
# operator on stdio: the run scope (required), {"companyIds":[...]} or {"all":true} (
|
|
60
|
-
#
|
|
63
|
+
# operator on stdio: the run scope (required), {"companyIds":[...]} or {"all":true} (any
|
|
64
|
+
# company). On HTTP set runScope per token instead.
|
|
61
65
|
# AUTOTASK_RUN_SCOPE={"companyIds":[12345]}
|
|
62
66
|
# operator on stdio: the principal plans and the audit log are bound to (default stdio).
|
|
63
67
|
# AUTOTASK_PRINCIPAL=nightly-time-entries
|
package/README.md
CHANGED
|
@@ -86,7 +86,7 @@ part miss never fails the call (typed `absent`); only a missing root record fail
|
|
|
86
86
|
**Every result is one envelope** (`structuredContent`, mirrored as JSON in the first text block):
|
|
87
87
|
|
|
88
88
|
```
|
|
89
|
-
{ status: ok|needs_input|ambiguous|
|
|
89
|
+
{ status: ok|needs_input|ambiguous|denied|error,
|
|
90
90
|
data, next: proceed|supply_input|choose_candidate|...|retry|stop,
|
|
91
91
|
reasons: [{ code, field?, detail }], retryable, correlationId, summary }
|
|
92
92
|
```
|
|
@@ -120,7 +120,7 @@ server metadata: [`server.json`](server.json) · per-workflow mapping:
|
|
|
120
120
|
|
|
121
121
|
| Variable | Default | Effect |
|
|
122
122
|
|----------|---------|--------|
|
|
123
|
-
| `AUTOTASK_PROFILE` | `readonly` | `readonly`: reads only
|
|
123
|
+
| `AUTOTASK_PROFILE` | `readonly` | `readonly`: reads only, no state. `operator`: adds `autotask_write` and `autotask_apply`; see [Operator profile](#operator-profile-writes). On HTTP it requires `AUTOTASK_HTTP_AUTH=tokens` or `gateway`. |
|
|
124
124
|
| `AUTOTASK_TOOLSETS` | — (core only) | comma list of `service`, `records`, `finance`, or `all`; an unknown name fails boot |
|
|
125
125
|
|
|
126
126
|
Every write and delete is refused on the read path (`autotask_read`) before any request
|
|
@@ -143,65 +143,44 @@ An operator deployment needs:
|
|
|
143
143
|
| Variable | Meaning |
|
|
144
144
|
|----------|---------|
|
|
145
145
|
| `AUTOTASK_PROFILE=operator` (or an operator token) | the profile |
|
|
146
|
-
| `
|
|
146
|
+
| `AUTOTASK_WRITE` | which writes are served and how: `allow`, `gate` or `deny` per operation, unlisted is read-only (grammar in [docs/write-gate.md](docs/write-gate.md)) |
|
|
147
147
|
| `AUTOTASK_STATE_DIR` | mode 0700; holds encrypted plans, the idempotency ledger, the hash-chained audit log, the primary lock and the holder registry (many processes may share one dir — see below) |
|
|
148
|
-
| `AUTOTASK_RUN_SCOPE` |
|
|
148
|
+
| `AUTOTASK_RUN_SCOPE` | `{"companyIds":[...]}` or `{"all":true}`: the run scope on stdio and in gateway mode (one scope shared by every gateway principal). In tokens mode each operator token carries `runScope` instead |
|
|
149
149
|
| `AUTOTASK_PRINCIPAL` | stdio: the name plans and the audit log are bound to (default `stdio`) |
|
|
150
150
|
| `AUTOTASK_KILL_FILE` | optional: while this file exists every preview and apply is denied (default `<state dir>/KILL`) |
|
|
151
151
|
|
|
152
|
-
What the
|
|
153
|
-
|
|
154
|
-
-
|
|
155
|
-
|
|
156
|
-
`
|
|
157
|
-
`
|
|
158
|
-
|
|
159
|
-
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
before the resolve cannot apply after it. It is not an
|
|
174
|
-
|
|
175
|
-
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
(apply verifies it; deny rules still win; one use per plan). A human decides with the CLI, run in their own
|
|
185
|
-
terminal as the server's OS user with `AUTOTASK_STATE_DIR` and `AUTOTASK_POLICY_FILE` set as the server has them:
|
|
186
|
-
`autotask-mcp keygen <approver-id> --ops <op,op|*>` (creates a passphrase-sealed ed25519 key, mode 0600, default
|
|
187
|
-
`~/.config/iit-mcp-autotask/approver.key` or `$AUTOTASK_APPROVER_KEY_FILE`, and prints the entry for
|
|
188
|
-
`approvals.approvers`), then `autotask-mcp approvals list|show|approve|reject <handle>`. `show` and `approve`
|
|
189
|
-
recompute the plan hash and snapshot hash from the stored plan and refuse on a mismatch or a changed policy; they
|
|
190
|
-
print only verified fields, and every agent-supplied string (arguments, the agent's reason) is JSON-encoded behind
|
|
191
|
-
`| ` inside a labelled untrusted block with control and bidi characters stripped. `approve` and `reject` need you to
|
|
192
|
-
type the first 8 characters of the plan hash and the key passphrase; the passphrase is read from `/dev/tty` only
|
|
193
|
-
(never the environment, arguments, a pipe or a file; there is no `--yes`), so the command refuses without a
|
|
194
|
-
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
|
|
195
|
-
pty, and on stdio the sealed key file can be attacked offline by anything running as the same user. On HTTP run it
|
|
196
|
-
where the state dir lives (for example `docker exec -it` as the server's user). On stdio this is a
|
|
197
|
-
guardrail against a mistaken agent, not a boundary against an adversarial agent running as the same OS
|
|
198
|
-
user; it is a real boundary only on HTTP with the server, its key material and the policy file isolated
|
|
199
|
-
from the agent. Any edit to the policy file (including `approvals`) changes the policy hash and voids
|
|
200
|
-
every outstanding plan. `data.manual` carries a fingerprint of the record (ids, ticket,
|
|
201
|
-
start time, hours, resource — numbers and instants only, never vendor text) and, when the pinned
|
|
202
|
-
SDK provides `operations.uiLink`, a clickable Autotask deep link; a person finishes the job in
|
|
203
|
-
Autotask. A policy that sets `approval: "none"` on such an operation changes nothing: boot logs a
|
|
204
|
-
warning and `doctor` prints it. There is no `autotask_delete` (D-12): deletes preview through `autotask_write`.
|
|
152
|
+
What the write access can and cannot do (full reference: [docs/write-gate.md](docs/write-gate.md)):
|
|
153
|
+
|
|
154
|
+
- `AUTOTASK_WRITE` lists operations, families (`tickets.*`) or `*`, each `allow` (applies without a
|
|
155
|
+
person), `gate` (a person confirms the apply) or `deny`. An operation not listed is `OP_NOT_ALLOWED`.
|
|
156
|
+
`AUTOTASK_DEDUP` names the duplicate-check fields for a create, `AUTOTASK_WRITE_LIMITS` caps writes
|
|
157
|
+
(`50/h,200/d`) and `AUTOTASK_TRUSTED_HOST_PROMPT` names the clients whose own permission prompt counts
|
|
158
|
+
as the confirmation. A bad entry fails boot.
|
|
159
|
+
- A gated apply is confirmed by a trusted host's own prompt, else by one confirm form (MCP elicitation);
|
|
160
|
+
a client that can do neither gets `GATE_UNSUPPORTED`, and a decline or dismissal `GATE_DECLINED`.
|
|
161
|
+
Nothing is sent in either case.
|
|
162
|
+
- Moving a time entry or ticket charge to another ticket (#107): `time_entries.moveToTicket` and
|
|
163
|
+
`ticket_charges.moveToTicket`. Preview `autotask_write {operation, args: [sourceId, toTicketId]}`, then
|
|
164
|
+
apply. The server creates the copy on the target ticket, verifies it and deletes the original (the
|
|
165
|
+
vendor ignores an update of `ticketID`): the new record gets a new id, create date and creator, and
|
|
166
|
+
notifications may fire. A move into another company or contract is always gated. An unfinished earlier
|
|
167
|
+
move of the same source blocks another until its ledger entry ages out (14 days) or a person clears it:
|
|
168
|
+
in a terminal, as the server's OS user, with `AUTOTASK_STATE_DIR` set, `autotask-mcp moves list` shows
|
|
169
|
+
the blocked sources and `autotask-mcp moves resolve <resource>:<id>` (after you checked both tickets in
|
|
170
|
+
Autotask; you type the source id, and the copy's id when the ledger names one) closes the ledger lines
|
|
171
|
+
as `MOVE_RESOLVED` and audits it (`move_resolved`). It refuses within 10 minutes of the move being sent
|
|
172
|
+
and, while the engine may still run, when the sending server is still registered or alive. A plan
|
|
173
|
+
previewed before the resolve cannot apply after it. It is not an MCP tool, and it is a guardrail, not a
|
|
174
|
+
boundary against an agent with a same-user shell. Billed, attached, approved, closed-target and
|
|
175
|
+
older-than-14-days sources are refused.
|
|
176
|
+
- Hard rules no setting overrides: `authenticate.*` is never served; operations the SDK flags
|
|
177
|
+
`requiresApproval` or sensitive (webhooks, `resources.transferOwnership`, `contacts.create`) can only
|
|
178
|
+
be `gate` or `deny`. An exact `allow` entry for one is a boot error; a family or `*` entry resolves it
|
|
179
|
+
to `gate`. Destructive and move operations can be `allow`ed (see the trust model in the doc).
|
|
180
|
+
- When a person must finish a job by hand (a move whose source delete the vendor refused), the result is
|
|
181
|
+
`next: manual_action` and `data.move.manualAction` carries what to do and, when the pinned SDK
|
|
182
|
+
provides `operations.uiLink`, a clickable Autotask deep link. There is no `autotask_delete` (D-12):
|
|
183
|
+
deletes preview through `autotask_write`.
|
|
205
184
|
- The company scope is read from the live records the write touches (the target, its parent, the
|
|
206
185
|
ticket, project, task or contract it references, and any `companyID` it sets), never from names
|
|
207
186
|
or titles. A reference that cannot be read is refused.
|
|
@@ -215,7 +194,7 @@ precomposed form after NFC (for example a lone U+0301) is refused `INPUT_INVALID
|
|
|
215
194
|
precomposed character.
|
|
216
195
|
|
|
217
196
|
Outcomes the caller acts on: `ok`/`proceed`; `needs_input`/`supply_input` (`missing` names the
|
|
218
|
-
field); `
|
|
197
|
+
field); `denied`/`stop`; `STATE_DRIFT` or `PLAN_EXPIRED` →
|
|
219
198
|
`repreview`; a failed write: `WRITE_FAILED` with `stop` (a deterministic rejection; it replays,
|
|
220
199
|
fix the input and use a new key) or `RATE_LIMITED` with `retry` (nothing was written: apply the
|
|
221
200
|
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`,
|
|
@@ -473,7 +452,7 @@ No aliases: old names fail as unknown tools.
|
|
|
473
452
|
| `quote_get`, `contact_full_profile`, `contract_full_profile`, `project_get_context`, `resource_workload` | same names (toolsets) |
|
|
474
453
|
| `configurationItem_profile`, `serviceCall_full_profile` | `configuration_item_profile`, `service_call_full_profile` |
|
|
475
454
|
| `write`, `delete`, `timeEntry_create` | `write {operation, args, idempotency_key?}` then `apply {plan}` (operator profile) |
|
|
476
|
-
| env `READ_ONLY` / `WRITE_POLICY` / `WRITE_ALLOW` / `SECRET_READS` | `AUTOTASK_PROFILE` + `
|
|
455
|
+
| env `READ_ONLY` / `WRITE_POLICY` / `WRITE_ALLOW` / `SECRET_READS` | `AUTOTASK_PROFILE` + `AUTOTASK_WRITE` + `AUTOTASK_STATE_DIR` + `AUTOTASK_RUN_SCOPE`; boot fails if an old variable is set |
|
|
477
456
|
| CLI default stdio, bind on all interfaces | http on `127.0.0.1` by default; local clients pass `--transport stdio` |
|
|
478
457
|
| results: per-tool shapes, errors as a text JSON payload | one envelope; the old payload is the envelope `data` |
|
|
479
458
|
|
|
@@ -488,12 +467,11 @@ No aliases: old names fail as unknown tools.
|
|
|
488
467
|
`tokens`); a server-held credential on HTTP needs `tokens`.
|
|
489
468
|
4. Read-only use needs nothing else. Pick toolsets with `AUTOTASK_TOOLSETS` if the agent used the
|
|
490
469
|
service, records or finance tools.
|
|
491
|
-
5. Writes (the operator profile): write a
|
|
492
|
-
`examples/policy.time-entries.json`: a time entry there needs `startDateTime` for its duplicate
|
|
470
|
+
5. Writes (the operator profile): set `AUTOTASK_WRITE` (see [docs/write-gate.md](docs/write-gate.md)) and `AUTOTASK_DEDUP` (a time entry needs `startDateTime` for its duplicate
|
|
493
471
|
check, Service tickets need `startDateTime` and `endDateTime`, and `billingCodeID` must be an active
|
|
494
472
|
general allocation code), create the state dir's parent, and set
|
|
495
|
-
`AUTOTASK_PROFILE=operator`, `
|
|
496
|
-
(`AUTOTASK_RUN_SCOPE` on stdio, `runScope` per token
|
|
473
|
+
`AUTOTASK_PROFILE=operator`, `AUTOTASK_WRITE`, `AUTOTASK_STATE_DIR` and a run scope
|
|
474
|
+
(`AUTOTASK_RUN_SCOPE` on stdio and in gateway mode, shared by every principal; `runScope` per token in tokens mode). The first boot creates the state
|
|
497
475
|
dir (0700), its key and its lock. There is nothing to migrate: 0.5.x kept no write state.
|
|
498
476
|
6. Update agent prompts and playbooks to the new tool names (table above) and to the envelope's
|
|
499
477
|
`status`/`next`.
|
|
@@ -513,7 +491,7 @@ preview/apply path; a plan previewed under 0.6.0 cannot be applied after a rollb
|
|
|
513
491
|
| `GET /health` | process alive — pre-auth, **no vendor call** (container healthcheck) | none |
|
|
514
492
|
| `GET /ready` | config valid, SDK plan-hash pin checked, server initialised, client factory ready — **no live vendor call** (credentials are request-scoped). In an operator deployment the payload also carries `lock` (the state lock's age, holder id, lease freshness and the holder count — see "The state lock: recovery") | none |
|
|
515
493
|
| `GET /doctor` | zero-wire credential/config preflight: per credential field `present`/`len`/`prefix2`/`shapeOk` (`AUTOTASK_SECRET`'s prefix is masked to `[REDACTED]` — no character of a secret is ever emitted; identifiers keep their format prefix), the base-url + allow-list resolution (`default_only` / `allow_list` / `default_and_list` / `any`), the SDK plan-hash gate status and the host-policy mode — **no vendor call, by construction** | Host/Origin validated; a bearer token in `tokens` mode |
|
|
516
|
-
| `GET /metadata` | server identity + the MCP protocol **era pair** (baseline `2026-07-28` + legacy fallback `2025-11-25`) + capability-manifest pointer + the `deployment` block (profile, toolsets,
|
|
494
|
+
| `GET /metadata` | server identity + the MCP protocol **era pair** (baseline `2026-07-28` + legacy fallback `2025-11-25`) + capability-manifest pointer + the `deployment` block (profile, toolsets, caller auth, rate limiting) — the served payload is the repo's `server.json` + build stamp | Host/Origin validated; a bearer token in `tokens` mode |
|
|
517
495
|
|
|
518
496
|
## Configuration
|
|
519
497
|
|
|
@@ -524,9 +502,12 @@ non-zero on an invalid config (fail-fast) instead of failing at the first tool c
|
|
|
524
502
|
|----------|----------|---------|---------|
|
|
525
503
|
| `MCP_TRANSPORT` | no | `http` | `stdio` or `http`; the CLI `--transport` flag overrides |
|
|
526
504
|
| `AUTOTASK_PROFILE` | no | `readonly` | `readonly` or `operator` (see [Profiles and writes](#profiles-and-writes)) |
|
|
527
|
-
| `
|
|
505
|
+
| `AUTOTASK_WRITE` | operator | — | per-operation `allow`/`gate`/`deny` (see [docs/write-gate.md](docs/write-gate.md)) |
|
|
506
|
+
| `AUTOTASK_DEDUP` | no | — | create duplicate-check fields, e.g. `companies.create:companyName` |
|
|
507
|
+
| `AUTOTASK_WRITE_LIMITS` | no | — | write caps, e.g. `50/h,200/d` |
|
|
508
|
+
| `AUTOTASK_TRUSTED_HOST_PROMPT` | no | — | `stdio` and client ids whose own prompt confirms a gated apply |
|
|
528
509
|
| `AUTOTASK_STATE_DIR` | operator | — | the operator state dir (0700) |
|
|
529
|
-
| `AUTOTASK_RUN_SCOPE` | operator on stdio | — | `{"companyIds":[...]}` or `{"all":true}` |
|
|
510
|
+
| `AUTOTASK_RUN_SCOPE` | operator on stdio or gateway | — | `{"companyIds":[...]}` or `{"all":true}` |
|
|
530
511
|
| `AUTOTASK_PRINCIPAL` | no | `stdio` | the stdio operator principal |
|
|
531
512
|
| `AUTOTASK_KILL_FILE` | no | `<state dir>/KILL` | the kill switch file |
|
|
532
513
|
| `AUTOTASK_USER_TIMEZONE` | no | `UTC` | IANA time zone of the people asking (their local "today"), not the Autotask tenant's or the server's zone; region/city names only (e.g. `Australia/Melbourne`), kept as written with only the case of a listed name fixed (`Europe/Kyiv` stays `Europe/Kyiv`); `utc`, `Etc/UTC` and `GMT` mean `UTC`; fixed offsets (`+10:00`, `Etc/GMT-10`) and names without a region (`EST`, `Japan`) fail boot. Autotask stores UTC; the zone is told to clients (`autotask_tenant_status` `time` part: zone, local time, UTC time, offset; one line in the server instructions when not UTC) so "today" and "yesterday" become UTC bounds in the right zone for moment fields such as ticket creation and due times. Time entries are calendar days: `dateWorked` and `time_entries.dateWorkedWindow` take plain `YYYY-MM-DD`, not UTC bounds. Filter values are never converted. |
|
|
@@ -549,7 +530,7 @@ non-zero on an invalid config (fail-fast) instead of failing at the first tool c
|
|
|
549
530
|
| `MCP_HOST` | no | `127.0.0.1` | http bind address (`--host` overrides it). Off loopback, `AUTOTASK_HTTP_ALLOWED_HOSTS` and `AUTOTASK_HTTP_AUTH` are required |
|
|
550
531
|
| `AUTOTASK_HTTP_ALLOWED_HOSTS` | off loopback | — | `Host` names `/mcp` accepts (DNS-rebinding defence) |
|
|
551
532
|
| `AUTOTASK_HTTP_ALLOWED_ORIGINS` | no | — | browser `Origin` hostnames `/mcp` accepts; a request without an `Origin` passes |
|
|
552
|
-
| `AUTOTASK_HTTP_AUTH` | off loopback; `tokens` for operator
|
|
533
|
+
| `AUTOTASK_HTTP_AUTH` | off loopback; `tokens` or `gateway` for operator, `tokens` for a server-held tuple on http | — | `gateway` or `tokens` |
|
|
553
534
|
| `AUTOTASK_HTTP_TOKENS_FILE` | with `tokens` | — | the tokens file (see [Gateway (http, container)](#gateway-http-container)) |
|
|
554
535
|
| `AUTOTASK_HTTP_RATE_PER_MIN` | no | `60` | requests a minute per principal in `tokens` mode |
|
|
555
536
|
| `PORT` | no | `8080` | http listener port (`--port` overrides it) |
|
|
@@ -855,7 +836,7 @@ malformed).
|
|
|
855
836
|
## Gateway assumptions
|
|
856
837
|
|
|
857
838
|
With `AUTOTASK_HTTP_AUTH=gateway` the gateway is the control plane. It owns: client authentication,
|
|
858
|
-
per-tenant tool filtering/exposure, **credential injection** into the `X-Autotask-*` headers and rate
|
|
839
|
+
per-tenant tool filtering/exposure, **credential injection** into the `X-Autotask-*` headers, the per-request identity headers `x-autotask-client-id` and `x-autotask-principal` (see [docs/write-gate.md](docs/write-gate.md#gateway-identity); the gateway must strip client-supplied copies) and rate
|
|
859
840
|
limiting (`/metadata` states the delegation). The server still validates `Host`/`Origin` and caps
|
|
860
841
|
bodies at 1 MiB. With `AUTOTASK_HTTP_AUTH=tokens` this process authenticates callers and
|
|
861
842
|
rate-limits per principal itself. Either way the vendor-side budget (10,000 req/hour, ≤10 concurrent
|
package/capabilities.json
CHANGED
|
@@ -10,11 +10,9 @@
|
|
|
10
10
|
},
|
|
11
11
|
"profiles": {
|
|
12
12
|
"readonly": {
|
|
13
|
-
"approvalMode": "off",
|
|
14
13
|
"note": "Reads only. The default."
|
|
15
14
|
},
|
|
16
15
|
"operator": {
|
|
17
|
-
"approvalMode": "policy",
|
|
18
16
|
"note": "Adds the operator toolset (preview with autotask_write, execute with autotask_apply)."
|
|
19
17
|
}
|
|
20
18
|
},
|
|
@@ -348,7 +346,7 @@
|
|
|
348
346
|
"type": "write",
|
|
349
347
|
"toolset": "operator",
|
|
350
348
|
"profile": "operator",
|
|
351
|
-
"backingOperation": "node-autotask/operations invokeWrite / invokeDelete dry run (the keys
|
|
349
|
+
"backingOperation": "node-autotask/operations invokeWrite / invokeDelete dry run (the keys AUTOTASK_WRITE lists)",
|
|
352
350
|
"annotations": {
|
|
353
351
|
"readOnly": true,
|
|
354
352
|
"idempotent": true,
|
|
@@ -380,7 +378,7 @@
|
|
|
380
378
|
"autotask_describe",
|
|
381
379
|
"autotask_read"
|
|
382
380
|
],
|
|
383
|
-
"backingRegistryOperations":
|
|
381
|
+
"backingRegistryOperations": 1225,
|
|
384
382
|
"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
383
|
},
|
|
386
384
|
"schemas": {}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type ClientCapabilities, type InputRequiredResult } from '@modelcontextprotocol/server';
|
|
2
|
+
import { type ToolReturn } from '../tools/results.js';
|
|
3
|
+
import type { Confirmation } from '../tools/write/apply-phases.js';
|
|
4
|
+
import type { PlanRecord } from '../write/store.js';
|
|
5
|
+
/** The key the confirm form's answer rides under in `inputResponses`. */
|
|
6
|
+
export declare const GATE_KEY = "confirm";
|
|
7
|
+
export type Caller = {
|
|
8
|
+
transport: 'stdio';
|
|
9
|
+
} | {
|
|
10
|
+
transport: 'http';
|
|
11
|
+
clientId?: string;
|
|
12
|
+
};
|
|
13
|
+
export type GateOutcome = {
|
|
14
|
+
ok: Confirmation;
|
|
15
|
+
} | {
|
|
16
|
+
result: ToolReturn | InputRequiredResult;
|
|
17
|
+
};
|
|
18
|
+
/** What the person reads: the op, its targets and the plan handle, then the data fenced as untrusted and capped. */
|
|
19
|
+
export declare function gateMessage(plan: PlanRecord): string;
|
|
20
|
+
/** Whether the calling client can show a form-mode elicitation. An empty `elicitation: {}` declares form mode. */
|
|
21
|
+
export declare function formElicitation(extra: unknown, caller: Caller, legacyCaps: ClientCapabilities | undefined): boolean;
|
|
22
|
+
/** Decide one gated plan's apply: a confirmation, the confirm form, or a typed deny. Zero vendor calls on every path. */
|
|
23
|
+
export declare function gateApply(a: {
|
|
24
|
+
plan: PlanRecord;
|
|
25
|
+
caller: Caller;
|
|
26
|
+
trusted: ReadonlySet<string>;
|
|
27
|
+
extra: unknown;
|
|
28
|
+
legacyCaps: ClientCapabilities | undefined;
|
|
29
|
+
}): GateOutcome;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* access/gate.ts — the apply-time human gate for a gated plan (#178).
|
|
3
|
+
*
|
|
4
|
+
* A trusted caller's own host prompt counts as the confirmation (AUTOTASK_TRUSTED_HOST_PROMPT). Any
|
|
5
|
+
* other caller whose client shows form elicitation gets one plain confirm form through the SDK's
|
|
6
|
+
* multi-round-trip `input_required` result, and the client retries the same call with the answer.
|
|
7
|
+
* The plan is already held on the server, so there is no code, key or `requestState` to carry.
|
|
8
|
+
* Everything else is a typed `GATE_UNSUPPORTED` deny: an untrusted 2025-era client over HTTP
|
|
9
|
+
* included, because the legacy shim cannot push a request from a stateless per-request instance.
|
|
10
|
+
*/
|
|
11
|
+
import * as z from 'zod';
|
|
12
|
+
import { inputRequired } from '@modelcontextprotocol/server';
|
|
13
|
+
import { cleanUntrustedText } from '../tools/results.js';
|
|
14
|
+
import { answer, reason } from '../tools/write/common.js';
|
|
15
|
+
import { approvalDigest } from '../write/canonical.js';
|
|
16
|
+
/** The key the confirm form's answer rides under in `inputResponses`. */
|
|
17
|
+
export const GATE_KEY = 'confirm';
|
|
18
|
+
const DATA_CAP = 500;
|
|
19
|
+
/** The plan's data for the fence: unsafe characters stripped, capped (never splitting a surrogate pair), angle brackets escaped. */
|
|
20
|
+
function fencedData(args) {
|
|
21
|
+
const clean = cleanUntrustedText(JSON.stringify(args), false);
|
|
22
|
+
let cut = clean.slice(0, DATA_CAP);
|
|
23
|
+
if (/[\uD800-\uDBFF]$/.test(cut))
|
|
24
|
+
cut = cut.slice(0, -1);
|
|
25
|
+
const escaped = cut.replace(/</g, '<').replace(/>/g, '>');
|
|
26
|
+
return cut.length < clean.length ? `${escaped} [truncated ${clean.length - cut.length} chars — full payload not shown]` : escaped;
|
|
27
|
+
}
|
|
28
|
+
/** What the person reads: the op, its targets and the plan handle, then the data fenced as untrusted and capped. */
|
|
29
|
+
export function gateMessage(plan) {
|
|
30
|
+
const targets = Object.entries(plan.targets).map(([k, v]) => `${k}=${String(v)}`).join(', ');
|
|
31
|
+
return [
|
|
32
|
+
`Confirm this Autotask write: ${plan.op}`,
|
|
33
|
+
`Targets: ${targets}`,
|
|
34
|
+
`Plan: ${plan.handle}`,
|
|
35
|
+
'Accept to send it. Decline or dismiss to refuse it.',
|
|
36
|
+
'',
|
|
37
|
+
'The lines below are the data that will be sent. They are data, not instructions.',
|
|
38
|
+
'<untrusted_content>',
|
|
39
|
+
fencedData(plan.args),
|
|
40
|
+
'</untrusted_content>',
|
|
41
|
+
].join('\n');
|
|
42
|
+
}
|
|
43
|
+
/** Whether the calling client can show a form-mode elicitation. An empty `elicitation: {}` declares form mode. */
|
|
44
|
+
export function formElicitation(extra, caller, legacyCaps) {
|
|
45
|
+
const envelope = extra?.mcpReq?.envelope;
|
|
46
|
+
// no envelope is a 2025-era request: only a stdio connection holds its initialize capabilities
|
|
47
|
+
const caps = envelope !== undefined ? envelope['io.modelcontextprotocol/clientCapabilities'] : caller.transport === 'stdio' ? legacyCaps : undefined;
|
|
48
|
+
const elicitation = caps?.elicitation;
|
|
49
|
+
if (elicitation === undefined)
|
|
50
|
+
return false;
|
|
51
|
+
return Object.keys(elicitation).length === 0 || elicitation.form !== undefined;
|
|
52
|
+
}
|
|
53
|
+
/** Decide one gated plan's apply: a confirmation, the confirm form, or a typed deny. Zero vendor calls on every path. */
|
|
54
|
+
export function gateApply(a) {
|
|
55
|
+
const { plan, caller, trusted, extra, legacyCaps } = a;
|
|
56
|
+
const digest = approvalDigest(plan.planHash, plan.facts);
|
|
57
|
+
const deny = (code, detail) => ({
|
|
58
|
+
result: answer('denied', 'stop', [reason(code, detail)], { operation: plan.op, targets: plan.targets, plan: plan.handle }, { plan: plan.handle, planHash: plan.planHash }),
|
|
59
|
+
});
|
|
60
|
+
if ((caller.transport === 'stdio' && trusted.has('stdio')) || (caller.transport === 'http' && caller.clientId !== undefined && trusted.has(caller.clientId))) {
|
|
61
|
+
return { ok: { via: 'host_prompt', digest } };
|
|
62
|
+
}
|
|
63
|
+
// an answer counts only from a client that declared form elicitation; it is untrusted input, so only an explicit accept confirms
|
|
64
|
+
if (formElicitation(extra, caller, legacyCaps)) {
|
|
65
|
+
const response = extra?.mcpReq?.inputResponses?.[GATE_KEY];
|
|
66
|
+
if (response !== undefined) {
|
|
67
|
+
if (response?.action === 'accept')
|
|
68
|
+
return { ok: { via: 'elicitation', digest } };
|
|
69
|
+
return deny('GATE_DECLINED', `the person declined or dismissed ${plan.op}; nothing was sent`);
|
|
70
|
+
}
|
|
71
|
+
return { result: inputRequired({ inputRequests: { [GATE_KEY]: inputRequired.elicit({ message: gateMessage(plan), requestedSchema: z.object({}) }) } }) };
|
|
72
|
+
}
|
|
73
|
+
return deny('GATE_UNSUPPORTED', `${plan.op} needs a person to confirm it, and this client cannot show a confirm form (and is not a trusted host); nothing was sent`);
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=gate.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type WriteShape } from '../write/hard-rules.js';
|
|
2
|
+
export type WriteMode = 'allow' | 'gate' | 'deny';
|
|
3
|
+
export interface ResolvedOp {
|
|
4
|
+
mode: WriteMode;
|
|
5
|
+
rule: string;
|
|
6
|
+
shape: WriteShape;
|
|
7
|
+
upgraded?: true;
|
|
8
|
+
dedup?: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
export interface WriteLimits {
|
|
11
|
+
perHour?: number;
|
|
12
|
+
perDay?: number;
|
|
13
|
+
}
|
|
14
|
+
export interface LoadedPolicy {
|
|
15
|
+
table: ReadonlyMap<string, ResolvedOp>;
|
|
16
|
+
limits?: WriteLimits;
|
|
17
|
+
policyHash: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function resolveWritePolicy(env: {
|
|
20
|
+
write: string;
|
|
21
|
+
dedup?: string;
|
|
22
|
+
limits?: string;
|
|
23
|
+
}): LoadedPolicy;
|
|
24
|
+
/** The trusted host-prompt ids (`stdio` or a connection id), comma separated. */
|
|
25
|
+
export declare function parseTrustedHosts(raw: string | undefined): ReadonlySet<string>;
|
|
26
|
+
/** A `gate` row, or a non-deny move row (a move can escalate to a gate). */
|
|
27
|
+
export declare const anyGate: (loaded: LoadedPolicy) => boolean;
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* access/write-env.ts — parse and resolve the env write grammar: `AUTOTASK_WRITE` (per-operation
|
|
3
|
+
* allow/gate/deny), `AUTOTASK_DEDUP`, `AUTOTASK_WRITE_LIMITS` and `AUTOTASK_TRUSTED_HOST_PROMPT`.
|
|
4
|
+
* Pure: resolution needs only the capability registry. Every problem is collected into one ConfigError.
|
|
5
|
+
*/
|
|
6
|
+
import { ConfigError } from '../config/error.js';
|
|
7
|
+
import { hashOf } from '../write/canonical.js';
|
|
8
|
+
import { hardRuleFor, MOVE_OPS, writeShape } from '../write/hard-rules.js';
|
|
9
|
+
import { RECORDS, recordById } from '../tools/catalog-data.js';
|
|
10
|
+
const FAMILY = /^[a-z][a-z0-9_]*\.\*$/;
|
|
11
|
+
const EXACT = /^[A-Za-z][A-Za-z0-9_]*\.[A-Za-z][A-Za-z0-9]*$/;
|
|
12
|
+
const FIELD = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
13
|
+
const LIMIT = /^([1-9]\d{0,5})\/(h|d)$/;
|
|
14
|
+
const TRUSTED = /^[A-Za-z0-9._:-]{1,200}$/;
|
|
15
|
+
/** Operations the preview/apply path can serve: real writes with a snapshot-able shape. */
|
|
16
|
+
const servable = () => {
|
|
17
|
+
const out = new Map();
|
|
18
|
+
for (const r of RECORDS) {
|
|
19
|
+
const shape = writeShape(r);
|
|
20
|
+
if (hardRuleFor(r.id, r).never === undefined && shape !== undefined)
|
|
21
|
+
out.set(r.id, shape);
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
const fail = (name, problems) => {
|
|
26
|
+
if (problems.length > 0)
|
|
27
|
+
throw new ConfigError(`${name} is invalid:\n ${problems.join('\n ')}`);
|
|
28
|
+
};
|
|
29
|
+
/** `*:gate,tickets.*:allow,...` -> pattern -> mode, rejecting malformed entries. */
|
|
30
|
+
function parseWrite(raw, ops, problems) {
|
|
31
|
+
const rules = new Map();
|
|
32
|
+
for (const entry of raw.split(',').map((e) => e.trim())) {
|
|
33
|
+
if (entry === '') {
|
|
34
|
+
problems.push('empty entry');
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const cut = entry.lastIndexOf(':');
|
|
38
|
+
const pattern = (cut < 0 ? entry : entry.slice(0, cut)).trim();
|
|
39
|
+
const mode = cut < 0 ? '' : entry.slice(cut + 1).trim();
|
|
40
|
+
if (mode !== 'allow' && mode !== 'gate' && mode !== 'deny') {
|
|
41
|
+
problems.push(`${entry}: mode must be allow, gate or deny`);
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
const exact = pattern !== '*' && !FAMILY.test(pattern) && EXACT.test(pattern);
|
|
45
|
+
if (pattern !== '*' && !FAMILY.test(pattern) && !exact) {
|
|
46
|
+
problems.push(`${pattern}: not *, family.* or an operation key`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (rules.has(pattern)) {
|
|
50
|
+
problems.push(`${pattern}: listed twice`);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if (FAMILY.test(pattern) && ![...ops.keys()].some((k) => k.startsWith(pattern.slice(0, -1)))) {
|
|
54
|
+
problems.push(`${pattern}: matches no servable write`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (exact && !ops.has(pattern)) {
|
|
58
|
+
const rec = recordById(pattern);
|
|
59
|
+
const never = hardRuleFor(pattern, rec).never;
|
|
60
|
+
problems.push(`${pattern}: ${never ?? 'the argument shape is not supported by preview/apply'}`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (exact && mode === 'allow' && hardRuleFor(pattern, recordById(pattern)).approval === 'always') {
|
|
64
|
+
problems.push(`${pattern}: always needs a person; use gate`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
rules.set(pattern, mode);
|
|
68
|
+
}
|
|
69
|
+
return rules;
|
|
70
|
+
}
|
|
71
|
+
function parseLimits(raw, problems) {
|
|
72
|
+
if (raw === undefined)
|
|
73
|
+
return undefined;
|
|
74
|
+
const limits = {};
|
|
75
|
+
for (const entry of raw.split(',').map((e) => e.trim())) {
|
|
76
|
+
const m = LIMIT.exec(entry);
|
|
77
|
+
if (!m) {
|
|
78
|
+
problems.push(`${entry === '' ? 'empty entry' : entry}: use <n>/h or <n>/d, n from 1 to 999999`);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const key = m[2] === 'h' ? 'perHour' : 'perDay';
|
|
82
|
+
if (limits[key] !== undefined)
|
|
83
|
+
problems.push(`${entry}: ${m[2]} listed twice`);
|
|
84
|
+
limits[key] = Number(m[1]);
|
|
85
|
+
}
|
|
86
|
+
return limits;
|
|
87
|
+
}
|
|
88
|
+
export function resolveWritePolicy(env) {
|
|
89
|
+
const ops = servable();
|
|
90
|
+
const problems = [];
|
|
91
|
+
const rules = parseWrite(env.write, ops, problems);
|
|
92
|
+
fail('AUTOTASK_WRITE', problems);
|
|
93
|
+
const table = new Map();
|
|
94
|
+
for (const [key, shape] of [...ops].sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
95
|
+
const family = `${key.slice(0, key.indexOf('.'))}.*`;
|
|
96
|
+
const rule = [key, family, '*'].find((p) => rules.has(p));
|
|
97
|
+
if (rule === undefined)
|
|
98
|
+
continue;
|
|
99
|
+
let mode = rules.get(rule);
|
|
100
|
+
const row = { mode, rule, shape };
|
|
101
|
+
if (mode === 'allow' && hardRuleFor(key, recordById(key)).approval === 'always') {
|
|
102
|
+
mode = 'gate';
|
|
103
|
+
row.mode = mode;
|
|
104
|
+
row.upgraded = true;
|
|
105
|
+
}
|
|
106
|
+
table.set(key, row);
|
|
107
|
+
}
|
|
108
|
+
const dedupProblems = [];
|
|
109
|
+
if (env.dedup !== undefined) {
|
|
110
|
+
const seen = new Set();
|
|
111
|
+
for (const entry of env.dedup.split(',').map((e) => e.trim())) {
|
|
112
|
+
if (entry === '') {
|
|
113
|
+
dedupProblems.push('empty entry');
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const cut = entry.indexOf(':');
|
|
117
|
+
const op = (cut < 0 ? entry : entry.slice(0, cut)).trim();
|
|
118
|
+
const fields = cut < 0 ? [''] : entry.slice(cut + 1).trim().split('+');
|
|
119
|
+
if (seen.has(op)) {
|
|
120
|
+
dedupProblems.push(`${op}: listed twice`);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
seen.add(op);
|
|
124
|
+
const row = table.get(op);
|
|
125
|
+
if (row === undefined || row.mode === 'deny') {
|
|
126
|
+
dedupProblems.push(`${op}: not served (absent or denied in AUTOTASK_WRITE)`);
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (row.shape !== 'create' && row.shape !== 'create_child') {
|
|
130
|
+
dedupProblems.push(`${op}: dedup applies to creates only`);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
if (!fields.every((f) => FIELD.test(f))) {
|
|
134
|
+
dedupProblems.push(`${op}: each field must be a plain field name joined by +`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
table.set(op, { ...row, dedup: fields });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
fail('AUTOTASK_DEDUP', dedupProblems);
|
|
141
|
+
const limitProblems = [];
|
|
142
|
+
const limits = parseLimits(env.limits, limitProblems);
|
|
143
|
+
fail('AUTOTASK_WRITE_LIMITS', limitProblems);
|
|
144
|
+
const policyHash = hashOf({
|
|
145
|
+
ops: Object.fromEntries([...table].map(([k, v]) => [k, { mode: v.mode, dedup: v.dedup ?? null }])),
|
|
146
|
+
limits: limits ?? null,
|
|
147
|
+
});
|
|
148
|
+
return { table, ...(limits ? { limits } : {}), policyHash };
|
|
149
|
+
}
|
|
150
|
+
/** The trusted host-prompt ids (`stdio` or a connection id), comma separated. */
|
|
151
|
+
export function parseTrustedHosts(raw) {
|
|
152
|
+
if (raw === undefined)
|
|
153
|
+
return new Set();
|
|
154
|
+
const bad = raw.split(',').map((e) => e.trim()).filter((e) => !TRUSTED.test(e));
|
|
155
|
+
if (bad.length > 0)
|
|
156
|
+
throw new ConfigError(`AUTOTASK_TRUSTED_HOST_PROMPT is invalid: ${bad.map((b) => JSON.stringify(b)).join(', ')} (letters, digits and . _ : - only)`);
|
|
157
|
+
return new Set(raw.split(',').map((e) => e.trim()));
|
|
158
|
+
}
|
|
159
|
+
/** A `gate` row, or a non-deny move row (a move can escalate to a gate). */
|
|
160
|
+
export const anyGate = (loaded) => [...loaded.table].some(([k, v]) => v.mode === 'gate' || (v.mode !== 'deny' && MOVE_OPS.includes(k)));
|
|
161
|
+
//# sourceMappingURL=write-env.js.map
|
package/dist/auth.d.ts
CHANGED
|
@@ -32,6 +32,10 @@ export declare const AUTOTASK_SECRET_HEADER = "x-autotask-secret";
|
|
|
32
32
|
export declare const AUTOTASK_IMPERSONATION_HEADER = "x-autotask-impersonation-resource-id";
|
|
33
33
|
/** The header carrying the caller's zone base URL (optional; allow-listed). */
|
|
34
34
|
export declare const AUTOTASK_BASE_URL_HEADER = "x-autotask-base-url";
|
|
35
|
+
/** The gateway-validated client id (`AUTOTASK_HTTP_AUTH=gateway` only; matched against AUTOTASK_TRUSTED_HOST_PROMPT). */
|
|
36
|
+
export declare const GATEWAY_CLIENT_ID_HEADER = "x-autotask-client-id";
|
|
37
|
+
/** The gateway-validated per-user principal (`AUTOTASK_HTTP_AUTH=gateway` only; the Entra oid). */
|
|
38
|
+
export declare const GATEWAY_PRINCIPAL_HEADER = "x-autotask-principal";
|
|
35
39
|
/** The headers that carry per-request scope, and so must never be read from a duplicated pair. */
|
|
36
40
|
export declare const CREDENTIAL_HEADERS: readonly string[];
|
|
37
41
|
/** The caller's per-request identity, exactly as the transport supplied it. */
|
|
@@ -44,6 +48,15 @@ export interface RequestScope {
|
|
|
44
48
|
}
|
|
45
49
|
/** Read the caller's per-request scope from request headers (all-or-nothing values, see module doc). */
|
|
46
50
|
export declare function readRequestScope(headers: Record<string, string | string[] | undefined>): RequestScope;
|
|
51
|
+
/**
|
|
52
|
+
* Read the identity the gateway stamped on the request. Callers must read it ONLY in gateway
|
|
53
|
+
* mode: anywhere else these headers are caller-controlled and mean nothing. Duplicated or
|
|
54
|
+
* malformed values are absent.
|
|
55
|
+
*/
|
|
56
|
+
export declare function readGatewayIdentity(headers: Record<string, string | string[] | undefined>): {
|
|
57
|
+
clientId?: string;
|
|
58
|
+
principal?: string;
|
|
59
|
+
};
|
|
47
60
|
/**
|
|
48
61
|
* How many of the three identity members this scope presents.
|
|
49
62
|
*
|
package/dist/auth.js
CHANGED
|
@@ -32,6 +32,10 @@ export const AUTOTASK_SECRET_HEADER = 'x-autotask-secret';
|
|
|
32
32
|
export const AUTOTASK_IMPERSONATION_HEADER = 'x-autotask-impersonation-resource-id';
|
|
33
33
|
/** The header carrying the caller's zone base URL (optional; allow-listed). */
|
|
34
34
|
export const AUTOTASK_BASE_URL_HEADER = 'x-autotask-base-url';
|
|
35
|
+
/** The gateway-validated client id (`AUTOTASK_HTTP_AUTH=gateway` only; matched against AUTOTASK_TRUSTED_HOST_PROMPT). */
|
|
36
|
+
export const GATEWAY_CLIENT_ID_HEADER = 'x-autotask-client-id';
|
|
37
|
+
/** The gateway-validated per-user principal (`AUTOTASK_HTTP_AUTH=gateway` only; the Entra oid). */
|
|
38
|
+
export const GATEWAY_PRINCIPAL_HEADER = 'x-autotask-principal';
|
|
35
39
|
/** The headers that carry per-request scope, and so must never be read from a duplicated pair. */
|
|
36
40
|
export const CREDENTIAL_HEADERS = [
|
|
37
41
|
AUTOTASK_INTEGRATION_CODE_HEADER,
|
|
@@ -77,6 +81,21 @@ export function readRequestScope(headers) {
|
|
|
77
81
|
...(base !== undefined ? { baseUrl: base } : {}),
|
|
78
82
|
};
|
|
79
83
|
}
|
|
84
|
+
const GATEWAY_ID = /^[A-Za-z0-9._@:-]{1,200}$/;
|
|
85
|
+
/**
|
|
86
|
+
* Read the identity the gateway stamped on the request. Callers must read it ONLY in gateway
|
|
87
|
+
* mode: anywhere else these headers are caller-controlled and mean nothing. Duplicated or
|
|
88
|
+
* malformed values are absent.
|
|
89
|
+
*/
|
|
90
|
+
export function readGatewayIdentity(headers) {
|
|
91
|
+
const valid = (name) => {
|
|
92
|
+
const v = usable(readSingleHeader(headers, name));
|
|
93
|
+
return v !== undefined && GATEWAY_ID.test(v) ? v : undefined;
|
|
94
|
+
};
|
|
95
|
+
const clientId = valid(GATEWAY_CLIENT_ID_HEADER);
|
|
96
|
+
const principal = valid(GATEWAY_PRINCIPAL_HEADER);
|
|
97
|
+
return { ...(clientId !== undefined ? { clientId } : {}), ...(principal !== undefined ? { principal } : {}) };
|
|
98
|
+
}
|
|
80
99
|
/**
|
|
81
100
|
* How many of the three identity members this scope presents.
|
|
82
101
|
*
|