iit-mcp-autotask 0.38.4 → 0.38.6
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 +1 -1
- package/README.md +19 -124
- package/dist/config/constants.d.ts +1 -6
- package/dist/config/constants.js +1 -16
- package/dist/config/credentials.d.ts +1 -1
- package/dist/config/credentials.js +2 -4
- package/dist/config/load.js +2 -7
- package/dist/config.d.ts +2 -2
- package/dist/config.js +2 -2
- package/dist/credential.d.ts +22 -7
- package/dist/credential.js +0 -10
- package/dist/main.js +5 -1
- package/dist/tools/catalog-data.d.ts +1 -1
- package/dist/tools/catalog-data.js +1 -1
- package/dist/tools/composites.js +3 -3
- package/dist/tools/context/client.d.ts +3 -0
- package/dist/tools/context/client.js +8 -1
- package/dist/tools/context.d.ts +2 -3
- package/dist/tools/context.js +1 -2
- package/dist/tools/dispatch.js +18 -15
- package/dist/tools/lists.js +2 -2
- package/dist/tools/output-file.d.ts +5 -8
- package/dist/tools/output-file.js +30 -79
- package/dist/tools/results/envelope.js +0 -2
- package/dist/tools/results/errors.d.ts +1 -25
- package/dist/tools/results/errors.js +4 -44
- package/dist/tools/scope-guard.d.ts +2 -1
- package/dist/tools/scope-guard.js +3 -1
- package/dist/tools/server-version.d.ts +2 -2
- package/dist/tools/server-version.js +12 -2
- package/dist/tools/server.d.ts +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/dist/mocks/fixture-ops.d.ts +0 -92
- package/dist/mocks/fixture-ops.js +0 -121
- package/dist/mocks/fixtures.d.ts +0 -100
- package/dist/mocks/fixtures.js +0 -94
- package/dist/mocks/harness.d.ts +0 -110
- package/dist/mocks/harness.js +0 -194
- package/dist/mocks/wire.d.ts +0 -74
- package/dist/mocks/wire.js +0 -183
- package/dist/tools/context/pool.d.ts +0 -5
- package/dist/tools/context/pool.js +0 -9
package/.env.example
CHANGED
|
@@ -77,7 +77,7 @@ AUTOTASK_PROFILE=readonly
|
|
|
77
77
|
# fixed offsets (+10:00, Etc/GMT-10) and names without a region (EST, Japan) are refused. An invalid name
|
|
78
78
|
# fails boot. Time entries stay calendar days (YYYY-MM-DD), not UTC bounds.
|
|
79
79
|
# AUTOTASK_USER_TIMEZONE=Australia/Sydney
|
|
80
|
-
# Removed in 0.6.0 (boot
|
|
80
|
+
# Removed in 0.6.0 (ignored with a boot warning, not honoured): AUTOTASK_READ_ONLY, AUTOTASK_WRITE_POLICY, AUTOTASK_WRITE_ALLOW,
|
|
81
81
|
# AUTOTASK_SECRET_READS. authenticate.* (a credential-minting read) is always refused.
|
|
82
82
|
|
|
83
83
|
# --- Per-tenant read cache (in-process; never cross-tenant; never on disk) ---
|
package/README.md
CHANGED
|
@@ -8,20 +8,11 @@ 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-29):** the runtime dependency `node-autotask` is published to public npm
|
|
12
|
-
> at `0.39.1`; the committed `pnpm-lock.yaml` pins that
|
|
13
|
-
> release with its integrity hash, so installs resolve deterministically. The dependency is always npm
|
|
14
|
-
> (node-autotask pinned exactly at `0.39.1`) — never a `file:`, `git:`, `link:` or workspace reference
|
|
15
|
-
> to a local checkout.
|
|
16
|
-
|
|
17
|
-
> **0.6.0 is a breaking release** (unreleased): tool names, result shapes and configuration all
|
|
18
|
-
> changed. See [Migrating from 0.5.x](#migrating-from-05x) and the [CHANGELOG](CHANGELOG.md).
|
|
19
|
-
|
|
20
11
|
## Tool surface
|
|
21
12
|
|
|
22
13
|
The **core** toolset (10 tools) is always served; `AUTOTASK_TOOLSETS` adds `service`, `records` and
|
|
23
14
|
`finance` (or `all`, 17 tools); the operator profile adds two. The whole tool surface (19 tools) is
|
|
24
|
-
listed below. Every other operation in the SDK's
|
|
15
|
+
listed below. Every other operation in the SDK's operation registry stays
|
|
25
16
|
reachable through the generic layer: `autotask_catalog` finds the key, `autotask_describe` serves
|
|
26
17
|
its contract (slim by default: transport and retry detail dropped, long text cut, the first two
|
|
27
18
|
examples; `full: true` serves the complete record, `summary: true` about 2 KB), `autotask_read` runs it.
|
|
@@ -147,8 +138,8 @@ fields on writes: [`docs/company-location-fields.md`](docs/company-location-fiel
|
|
|
147
138
|
Every write and delete is refused on the read path (`autotask_read`) before any request
|
|
148
139
|
(`POLICY_DENIED`, status `denied`), on every profile. `authenticate.*` returns a live vendor token
|
|
149
140
|
(credential-minting) and is never served, on any profile, through any tool. The 0.5.x variables `AUTOTASK_READ_ONLY`, `AUTOTASK_WRITE_POLICY`,
|
|
150
|
-
`AUTOTASK_WRITE_ALLOW` and `AUTOTASK_SECRET_READS` were removed:
|
|
151
|
-
|
|
141
|
+
`AUTOTASK_WRITE_ALLOW` and `AUTOTASK_SECRET_READS` were removed: they are ignored (the server logs a
|
|
142
|
+
warning naming any unrecognised `AUTOTASK_*` variable at boot), so the profile governs.
|
|
152
143
|
|
|
153
144
|
### Operator profile (writes)
|
|
154
145
|
|
|
@@ -451,55 +442,6 @@ or in `.mcp.json` / Claude Desktop config:
|
|
|
451
442
|
**From a clone**: `pnpm i && pnpm build`, then point the client at
|
|
452
443
|
`node /abs/path/iit-mcp-autotask/dist/main.js --transport stdio`.
|
|
453
444
|
|
|
454
|
-
## Migrating from 0.5.x
|
|
455
|
-
|
|
456
|
-
No aliases: old names fail as unknown tools.
|
|
457
|
-
|
|
458
|
-
| 0.5.x | 0.6.0 |
|
|
459
|
-
|-------|-------|
|
|
460
|
-
| `catalog`, `describe {operation}`, `read {operation,args}`, `fetch_many`, `ticket_full_context`, `company_full_profile` | unchanged (core) |
|
|
461
|
-
| `read_batch {items}` / `relationship_map {resource_key}` | `read {items}` / `describe {resource}` |
|
|
462
|
-
| `resource_who_am_i`, `version`, `threshold_information` | `tenant_status {parts}` (`identity` + `impersonationResourceId`, `version`, `thresholds`, `server`, `time`) |
|
|
463
|
-
| `resolve_any` | `resolve {identifier, resources?}` |
|
|
464
|
-
| `search_across_resources {query,resources,limit}` | `resolve {identifier, match: "contains", resources, limit}` |
|
|
465
|
-
| `company_` / `configurationItems_` / `contact_` / `project_` / `ticket_resolve` | `resolve {identifier, resource}` |
|
|
466
|
-
| `ticket_find_by_ticket_number`, `configurationItems_find_by_serial`, `contact_find_by_email` | `resolve {identifier, resource, by: "ticket_number" \| "serial" \| "email"}` |
|
|
467
|
-
| `ticket_` / `timeEntry_` / `serviceCall_` / `contract_` / `timeOffRequest_query` | `query {resource, filter, ...}` |
|
|
468
|
-
| `ticket_sla_health_check`, `company_unbilled_work`, `contract_profitability`, `aggregate` | `read {operation: "tickets.slaHealthCheck" \| "companies.unbilledWork" \| "contracts.profitability" \| "operations.aggregate", args}` + `autotask_guide` |
|
|
469
|
-
| `invoice_get {id}` / `invoice_markup {id, format}` | `invoice_get {id, format}` (finance) |
|
|
470
|
-
| `quote_get`, `contact_full_profile`, `contract_full_profile`, `project_get_context`, `resource_workload` | same names (toolsets) |
|
|
471
|
-
| `configurationItem_profile`, `serviceCall_full_profile` | `configuration_item_profile`, `service_call_full_profile` |
|
|
472
|
-
| `write`, `delete`, `timeEntry_create` | `write {operation, args, idempotency_key?}` then `apply {plan}` (operator profile) |
|
|
473
|
-
| 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 |
|
|
474
|
-
| CLI default stdio, bind on all interfaces | http on `127.0.0.1` by default; local clients pass `--transport stdio` |
|
|
475
|
-
| results: per-tool shapes, errors as a text JSON payload | one envelope; the old payload is the envelope `data` |
|
|
476
|
-
|
|
477
|
-
**Upgrading a 0.5.x deployment, step by step:**
|
|
478
|
-
|
|
479
|
-
1. Remove `AUTOTASK_READ_ONLY`, `AUTOTASK_WRITE_POLICY`, `AUTOTASK_WRITE_ALLOW` and
|
|
480
|
-
`AUTOTASK_SECRET_READS` from the environment: 0.6.0 refuses to boot while one is set, and names
|
|
481
|
-
the replacement.
|
|
482
|
-
2. Local (stdio) clients: add `--transport stdio` (or `MCP_TRANSPORT=stdio`). Without it 0.6.0
|
|
483
|
-
starts HTTP on `127.0.0.1:8080` and the client hangs on stdin.
|
|
484
|
-
3. HTTP off loopback: set `AUTOTASK_HTTP_ALLOWED_HOSTS` and `AUTOTASK_HTTP_AUTH` (`gateway` or
|
|
485
|
-
`tokens`); a server-held credential on HTTP needs `tokens`.
|
|
486
|
-
4. Read-only use needs nothing else. Pick toolsets with `AUTOTASK_TOOLSETS` if the agent used the
|
|
487
|
-
service, records or finance tools.
|
|
488
|
-
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
|
|
489
|
-
check, Service tickets need `startDateTime` and `endDateTime`, and `billingCodeID` must be an active
|
|
490
|
-
general allocation code), create the state dir's parent, and set
|
|
491
|
-
`AUTOTASK_PROFILE=operator`, `AUTOTASK_WRITE`, `AUTOTASK_STATE_DIR` and a run scope
|
|
492
|
-
(`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
|
|
493
|
-
dir (0700), its key and its lock. There is nothing to migrate: 0.5.x kept no write state.
|
|
494
|
-
6. Update agent prompts and playbooks to the new tool names (table above) and to the envelope's
|
|
495
|
-
`status`/`next`.
|
|
496
|
-
|
|
497
|
-
**Rolling back to 0.5.x:** pin the package back to version 0.5.0, restore the 0.5.x variables you
|
|
498
|
-
removed in step 1 (and drop the 0.6.0 ones: 0.5.x ignores them but nothing should depend on them),
|
|
499
|
-
and remove `--transport stdio` only if your client never passed a transport (0.5.x defaulted to
|
|
500
|
-
stdio). Keep the operator state dir: its `audit.jsonl` is the record of every write 0.6.0 made, and
|
|
501
|
-
0.5.x neither reads nor touches it. Plans and the ledger are not portable to 0.5.x, which has no
|
|
502
|
-
preview/apply path; a plan previewed under 0.6.0 cannot be applied after a rollback.
|
|
503
445
|
|
|
504
446
|
## Endpoints
|
|
505
447
|
|
|
@@ -694,7 +636,7 @@ negative-cached or replayed.
|
|
|
694
636
|
|
|
695
637
|
## Per-tenant pool & cache
|
|
696
638
|
|
|
697
|
-
The stateless default is amended by one **user-approved deviation** (
|
|
639
|
+
The stateless default is amended by one **user-approved deviation** (user-approved 2026-10-06;
|
|
698
640
|
security decision D-1 ratified): a bounded **in-process** client pool with a per-tenant TTL read
|
|
699
641
|
cache, because a fresh client per request would silently disable the SDK's local vendor-budget
|
|
700
642
|
guards (rate limiter, 429 cooldown) — a worse security posture, not a safer one.
|
|
@@ -775,39 +717,23 @@ guards (rate limiter, 429 cooldown) — a worse security posture, not a safer on
|
|
|
775
717
|
scans the image. This repo ships no GitHub Actions workflow; quality gates and publishing are
|
|
776
718
|
run manually by the maintainer.
|
|
777
719
|
|
|
778
|
-
Full control set: [`docs/architecture.md`](docs/architecture.md) (sections 7.3, 8 and 10)
|
|
779
|
-
independent review with its resolutions, [`docs/reviews/0.6.0.md`](docs/reviews/0.6.0.md).
|
|
720
|
+
Full control set: [`docs/architecture.md`](docs/architecture.md) (sections 7.3, 8 and 10).
|
|
780
721
|
|
|
781
722
|
## SDK dependency
|
|
782
723
|
|
|
783
|
-
`iit-mcp-autotask` depends on **`node-autotask` from npm at
|
|
784
|
-
no range operator
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
freezes the pnpm/git/Docker paths, the pin freezes the npm/npx paths. The docs are written against this
|
|
797
|
-
published-package baseline, and the pre-publish checklist still starts with a registry re-check
|
|
798
|
-
(`npm view node-autotask version`) — which now passes.
|
|
799
|
-
|
|
800
|
-
**Plan-hash drift gate.** At boot, before the listener binds, the installed `node-autotask`'s
|
|
801
|
-
capability plan hash must equal the pin this surface was built and reviewed against
|
|
802
|
-
(`assertPlanHash`); a mismatch refuses the boot naming the drifted groups. The server only runs
|
|
803
|
-
against the SDK build it was reviewed against — a plan-changing SDK update can reach a user only
|
|
804
|
-
through a deliberately re-pinned server release, so drift is refused at boot, not discovered
|
|
805
|
-
mid-run.
|
|
806
|
-
|
|
807
|
-
**Upgrading node-autotask** is a deliberate server release (pin bump, plan-hash re-pin, mirrored-surface
|
|
808
|
-
re-review, gates) — not an npm-range float. The exact steps live in the [per-release
|
|
809
|
-
checklist](CONTRIBUTING.md#per-release-checklist-node-autotask-upgrade) in CONTRIBUTING.md, and `pnpm
|
|
810
|
-
fresh-install-check` re-proves the fresh-install property before every publish.
|
|
724
|
+
`iit-mcp-autotask` depends on **`node-autotask` from npm at an exact pin** (see `package.json`;
|
|
725
|
+
no range operator) - the only vendor dependency; the others are `@modelcontextprotocol/server`
|
|
726
|
+
and `zod`. It never depends on a checkout (no `file:`, `git:`, `link:` or workspace references).
|
|
727
|
+
The published tarball ships no lockfile, so a fresh `npx`/`npm` install must resolve to the release
|
|
728
|
+
this surface was reviewed against, or the plan-hash boot check refuses to start.
|
|
729
|
+
|
|
730
|
+
**Plan-hash drift gate.** At boot, before the listener binds, the installed SDK's capability plan
|
|
731
|
+
hash must equal the pin this surface was reviewed against (`assertPlanHash`); a mismatch refuses
|
|
732
|
+
the boot naming the drifted groups.
|
|
733
|
+
|
|
734
|
+
**Upgrading node-autotask** is a deliberate server release (pin bump, plan-hash re-pin,
|
|
735
|
+
mirrored-surface re-review, gates), not a range float: see the
|
|
736
|
+
[per-release checklist](CONTRIBUTING.md#per-release-checklist-node-autotask-upgrade).
|
|
811
737
|
|
|
812
738
|
## Development
|
|
813
739
|
|
|
@@ -862,35 +788,4 @@ rate-limits per principal itself. Either way the vendor-side budget (10,000 req/
|
|
|
862
788
|
per integration) is guarded by the SDK agent profile (8,000/hr local budget, 3 concurrent) and exposed
|
|
863
789
|
live through `autotask_tenant_status`.
|
|
864
790
|
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
None of the ten confirmed workflows is blocked. Confirmed absent from the SDK (recommend SDK
|
|
868
|
-
issues — do not implement in this server):
|
|
869
|
-
|
|
870
|
-
- **Knowledge base** — `KnowledgeBaseArticles` (43 paths) + `KnowledgeBaseCategories` (12): a real
|
|
871
|
-
SDK gap vs the vendored spec.
|
|
872
|
-
- **Warehouse inventory** — `InventoryItems` / `InventoryLocations` / `InventoryStockedItems{,Add,Remove,Transfer}` /
|
|
873
|
-
`InventoryTransfers` / `InventoryItemSerialNumbers` (the SDK covers `InventoryProducts` only).
|
|
874
|
-
- **Documents/articles ecosystem** — `Document*`/`Article*` family (the whole KB/document layer).
|
|
875
|
-
- **Sales & pricing** — `SalesOrders`, `PriceList*`, `Taxes`/`TaxCategories`/`TaxRegions`,
|
|
876
|
-
`Currencies`, `PaymentTerms`, `ShippingTypes`, standalone `ServiceBundles`.
|
|
877
|
-
- **Attachment downloads** — metadata-only surfaces for company/project/task/resource/expense
|
|
878
|
-
parents (the SDK covers `ticket_attachments` + `time_entry_attachments` downloads only).
|
|
879
|
-
|
|
880
|
-
Not in the vendor REST spec at all (vendor surface gaps, not SDK gaps): reporting/BI, estimates,
|
|
881
|
-
physical assets (Autotask models them as Configuration Items — covered).
|
|
882
|
-
|
|
883
|
-
## Known limitations & blockers
|
|
884
|
-
|
|
885
|
-
| # | Item | Status |
|
|
886
|
-
|---|------|--------|
|
|
887
|
-
| B1 | **Project phase & task lists** (W7 sub-capability): no `project_tasks`/`project_phases` operations in the SDK's 1,281-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 |
|
|
888
|
-
| 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 |
|
|
889
|
-
| B3 | **`time_entries.getPosted`/`getUnposted`: the zone's TimeEntry entity has no `postedDate`**, so SDK 0.31 and earlier 500ed (and `postedWithin`/`dateWorkedWindow` silently counted every entry unposted). SDK 0.32.0 derives posted state from a `billing_items.timeEntryID` join and returns `{ items, hasMore, window, approvedCount, unapprovedCount }` (optional date-only `dateFrom`/`dateTo`; no dates reads only the first 500 entries). Needs BillingItems read permission (one extra request per call); `approvedCount`/`unapprovedCount` are not posted state. SDK 0.37.0 adds `cursor`/`nextCursor` (page past 500 entries), `unconfirmed` (entries whose posted state is unknown, kept out of both counts) and `countOnly: true` on the four posted-state ops. Reachable via the generic layer. | resolved in SDK 0.32.0 (not re-verified live) |
|
|
890
|
-
| 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 |
|
|
891
|
-
| B5 | **Blind count/query surfaces on ww29** (companies not owned by the caller; tickets): count/query return 0/empty even when data exists. Not fixable client-side; the surface carries the typed blind-surface sentinel — `get(id)`/full-profile are the authorities. | zone limitation |
|
|
892
|
-
| B6 | **`node-autotask` npm publish**: the Phase-C install gate re-checks the registry; if still 404 the build pauses and escalates (SCOPING-resolved: the user publishes — no git/file dependency fallback). | open — user action |
|
|
893
|
-
|
|
894
|
-
Zone-anchored curation note (recon F5): several description pins are facts observed on ww29
|
|
895
|
-
(australia-nz) and are carried as zone-labelled pins, never silently deleted; on other tenants
|
|
896
|
-
they may be zone/user-specific.
|
|
791
|
+
See [docs/architecture.md](docs/architecture.md) section 5 for intentional omissions and blocked capabilities.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The closed value sets of the configuration: transports, profiles, toolsets, cache presets
|
|
1
|
+
/** The closed value sets of the configuration: transports, profiles, toolsets, cache presets (issue #91). */
|
|
2
2
|
/** `stdio` = local path (Claude Code / Claude Desktop / VS Code); `http` = remote (gateway). */
|
|
3
3
|
export declare const TRANSPORTS: readonly ["stdio", "http"];
|
|
4
4
|
export type Transport = (typeof TRANSPORTS)[number];
|
|
@@ -12,11 +12,6 @@ export type Profile = (typeof PROFILES)[number];
|
|
|
12
12
|
/** The optional toolsets `AUTOTASK_TOOLSETS` selects; `core` is always registered. */
|
|
13
13
|
export declare const TOOLSETS: readonly ["service", "records", "finance"];
|
|
14
14
|
export type Toolset = (typeof TOOLSETS)[number];
|
|
15
|
-
/**
|
|
16
|
-
* The variables since removed, with the replacement each boot refusal names. Boot fails when
|
|
17
|
-
* any is set: a deployment that still sets one expects a behaviour this build no longer has.
|
|
18
|
-
*/
|
|
19
|
-
export declare const REMOVED_VARIABLES: Readonly<Record<string, string>>;
|
|
20
15
|
/** The per-tenant read-cache preset every client this server builds gets. */
|
|
21
16
|
export declare const CACHE_PRESETS: readonly ["on", "off"];
|
|
22
17
|
export type CachePreset = (typeof CACHE_PRESETS)[number];
|
package/dist/config/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** The closed value sets of the configuration: transports, profiles, toolsets, cache presets
|
|
1
|
+
/** The closed value sets of the configuration: transports, profiles, toolsets, cache presets (issue #91). */
|
|
2
2
|
/** `stdio` = local path (Claude Code / Claude Desktop / VS Code); `http` = remote (gateway). */
|
|
3
3
|
export const TRANSPORTS = ['stdio', 'http'];
|
|
4
4
|
/**
|
|
@@ -9,21 +9,6 @@ export const TRANSPORTS = ['stdio', 'http'];
|
|
|
9
9
|
export const PROFILES = ['readonly', 'operator'];
|
|
10
10
|
/** The optional toolsets `AUTOTASK_TOOLSETS` selects; `core` is always registered. */
|
|
11
11
|
export const TOOLSETS = ['service', 'records', 'finance'];
|
|
12
|
-
/**
|
|
13
|
-
* The variables since removed, with the replacement each boot refusal names. Boot fails when
|
|
14
|
-
* any is set: a deployment that still sets one expects a behaviour this build no longer has.
|
|
15
|
-
*/
|
|
16
|
-
export const REMOVED_VARIABLES = {
|
|
17
|
-
AUTOTASK_READ_ONLY: 'AUTOTASK_PROFILE=readonly (the default) or AUTOTASK_PROFILE=operator',
|
|
18
|
-
AUTOTASK_WRITE_POLICY: 'AUTOTASK_PROFILE=readonly (the default) or AUTOTASK_PROFILE=operator',
|
|
19
|
-
AUTOTASK_WRITE_ALLOW: 'AUTOTASK_PROFILE=operator (writes are governed by the operator write path)',
|
|
20
|
-
AUTOTASK_SECRET_READS: 'nothing: authenticate.* is always refused (credential-minting)',
|
|
21
|
-
AUTOTASK_ACCESS_FILE: 'AUTOTASK_WRITE (see docs/write-gate.md)',
|
|
22
|
-
AUTOTASK_ACCESS_KEY: 'AUTOTASK_WRITE (see docs/write-gate.md)',
|
|
23
|
-
AUTOTASK_TOOL_EXCLUDE: 'AUTOTASK_TOOLSETS for reads, AUTOTASK_WRITE for writes',
|
|
24
|
-
AUTOTASK_OPS_ALLOW: 'AUTOTASK_WRITE',
|
|
25
|
-
AUTOTASK_POLICY_FILE: 'AUTOTASK_WRITE, AUTOTASK_DEDUP and AUTOTASK_WRITE_LIMITS (see docs/write-gate.md)',
|
|
26
|
-
};
|
|
27
12
|
/** The per-tenant read-cache preset every client this server builds gets. */
|
|
28
13
|
export const CACHE_PRESETS = ['on', 'off'];
|
|
29
14
|
/** The vendor's hard cap on requests per hour per integration (the local limiter must not exceed it). */
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Config } from './schema.js';
|
|
2
|
-
/** The recognised AUTOTASK_* names: the config schema's AUTOTASK_ keys
|
|
2
|
+
/** The recognised AUTOTASK_* names: the config schema's AUTOTASK_ keys (derived, never hand-listed). */
|
|
3
3
|
export declare function recognisedAutotaskVars(): string[];
|
|
4
4
|
/** The set-but-unrecognised AUTOTASK_* env keys (stale 0.4.x-era vars the server never reads). */
|
|
5
5
|
export declare function unrecognisedAutotaskVars(env: Record<string, string | undefined>): string[];
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/** Server credential intake and defaults: recognised variables, the credential load, the stdio defaults (issue #91). */
|
|
2
2
|
import { loadAutotaskCredentialsFromEnv } from 'node-autotask';
|
|
3
|
-
import { REMOVED_VARIABLES } from './constants.js';
|
|
4
3
|
import { CONFIG_FIELDS } from './schema.js';
|
|
5
|
-
/** The recognised AUTOTASK_* names: the config schema's AUTOTASK_ keys
|
|
4
|
+
/** The recognised AUTOTASK_* names: the config schema's AUTOTASK_ keys (derived, never hand-listed). */
|
|
6
5
|
export function recognisedAutotaskVars() {
|
|
7
|
-
|
|
8
|
-
return [...new Set([...schemaKeys, ...Object.keys(REMOVED_VARIABLES)])].sort();
|
|
6
|
+
return Object.keys(CONFIG_FIELDS.shape).filter((k) => k.startsWith('AUTOTASK_')).sort();
|
|
9
7
|
}
|
|
10
8
|
/** The set-but-unrecognised AUTOTASK_* env keys (stale 0.4.x-era vars the server never reads). */
|
|
11
9
|
export function unrecognisedAutotaskVars(env) {
|
package/dist/config/load.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/** `loadConfig`: the environment in, a validated Config out (issue #91). */
|
|
2
2
|
import { MALFORMED_CREDENTIAL_REFUSAL, detectCredentialShape, malformedCredentialBootMessage } from '../credential.js';
|
|
3
|
-
import { REMOVED_VARIABLES } from './constants.js';
|
|
4
3
|
import { CONFIG_FIELDS, schema } from './schema.js';
|
|
5
4
|
import { unrecognisedAutotaskVars, loadServerCredentials } from './credentials.js';
|
|
6
5
|
import { ConfigError } from './error.js';
|
|
@@ -15,9 +14,6 @@ import { ConfigError } from './error.js';
|
|
|
15
14
|
* problem at once — a boot loop that surfaces one missing variable per restart wastes real minutes.
|
|
16
15
|
*/
|
|
17
16
|
export function loadConfig(env = process.env) {
|
|
18
|
-
const removed = Object.entries(REMOVED_VARIABLES)
|
|
19
|
-
.filter(([name]) => env[name] !== undefined)
|
|
20
|
-
.map(([name, use]) => ` ${name}: removed, use ${use}`);
|
|
21
17
|
const parsed = schema.safeParse(env);
|
|
22
18
|
// The pre-dispatch credential shape gate (issue #4a). Runs BEFORE the schema verdict is thrown,
|
|
23
19
|
// so a malformed load is reported alongside (or instead of) the structural problems rather than
|
|
@@ -41,10 +37,9 @@ export function loadConfig(env = process.env) {
|
|
|
41
37
|
// text served is this server's own, value-free: the loader's verdict and the load recipe.
|
|
42
38
|
shapeIssues.push(` ${intake.sdkProblem.code}: ${MALFORMED_CREDENTIAL_REFUSAL}`);
|
|
43
39
|
}
|
|
44
|
-
if (parsed.success && shapeIssues.length === 0
|
|
40
|
+
if (parsed.success && shapeIssues.length === 0)
|
|
45
41
|
return parsed.data;
|
|
46
42
|
const problems = [
|
|
47
|
-
...removed,
|
|
48
43
|
...(parsed.success ? [] : parsed.error.issues.map((i) => ` ${i.path.join('.') || '(config)'}: ${i.message}`)),
|
|
49
44
|
];
|
|
50
45
|
const all = [...problems, ...shapeIssues];
|
|
@@ -53,7 +48,7 @@ export function loadConfig(env = process.env) {
|
|
|
53
48
|
const unrecognised = unrecognisedAutotaskVars(env);
|
|
54
49
|
const section = [
|
|
55
50
|
'AUTOTASK_* env vars:',
|
|
56
|
-
` recognised and set: ${Object.keys(CONFIG_FIELDS.shape).filter((k) => k.startsWith('AUTOTASK_') && env[k] !== undefined).sort().join(', ') || 'none'} — the vars this server actually reads
|
|
51
|
+
` recognised and set: ${Object.keys(CONFIG_FIELDS.shape).filter((k) => k.startsWith('AUTOTASK_') && env[k] !== undefined).sort().join(', ') || 'none'} — the vars this server actually reads`,
|
|
57
52
|
...(unrecognised.length > 0
|
|
58
53
|
? [
|
|
59
54
|
` unrecognised (ignored by this server): ${unrecognised.join(', ')}`,
|
package/dist/config.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* credential discovered by a model mid-conversation is a far worse failure than a container that
|
|
8
8
|
* never came up.
|
|
9
9
|
*
|
|
10
|
-
* The credential model (
|
|
10
|
+
* The credential model (docs/architecture.md 7.2, approved): Autotask's three-header identity
|
|
11
11
|
* (`AUTOTASK_API_INTEGRATION_CODE` / `AUTOTASK_USERNAME` / `AUTOTASK_SECRET`) + the zone origin
|
|
12
12
|
* (`AUTOTASK_BASE_URL`; the base path `/atservicesrest` is fixed, never configurable) + an
|
|
13
13
|
* optional impersonation resource id. Under `stdio` the process environment IS the trust
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* without its own `X-Autotask-*` headers runs as this identity, and that identity is only ever
|
|
16
16
|
* presented to the `AUTOTASK_BASE_URL` default origin (never to a caller-named one).
|
|
17
17
|
*/
|
|
18
|
-
export { TRANSPORTS, PROFILES, TOOLSETS,
|
|
18
|
+
export { TRANSPORTS, PROFILES, TOOLSETS, CACHE_PRESETS, VENDOR_HOURLY_CAP } from './config/constants.js';
|
|
19
19
|
export type { Transport, Profile, Toolset, CachePreset } from './config/constants.js';
|
|
20
20
|
export type { Config } from './config/schema.js';
|
|
21
21
|
export { RUN_SCOPE, isLoopbackHost } from './config/parse.js';
|
package/dist/config.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* credential discovered by a model mid-conversation is a far worse failure than a container that
|
|
8
8
|
* never came up.
|
|
9
9
|
*
|
|
10
|
-
* The credential model (
|
|
10
|
+
* The credential model (docs/architecture.md 7.2, approved): Autotask's three-header identity
|
|
11
11
|
* (`AUTOTASK_API_INTEGRATION_CODE` / `AUTOTASK_USERNAME` / `AUTOTASK_SECRET`) + the zone origin
|
|
12
12
|
* (`AUTOTASK_BASE_URL`; the base path `/atservicesrest` is fixed, never configurable) + an
|
|
13
13
|
* optional impersonation resource id. Under `stdio` the process environment IS the trust
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* without its own `X-Autotask-*` headers runs as this identity, and that identity is only ever
|
|
16
16
|
* presented to the `AUTOTASK_BASE_URL` default origin (never to a caller-named one).
|
|
17
17
|
*/
|
|
18
|
-
export { TRANSPORTS, PROFILES, TOOLSETS,
|
|
18
|
+
export { TRANSPORTS, PROFILES, TOOLSETS, CACHE_PRESETS, VENDOR_HOURLY_CAP } from './config/constants.js';
|
|
19
19
|
export { RUN_SCOPE, isLoopbackHost } from './config/parse.js';
|
|
20
20
|
export { recognisedAutotaskVars, unrecognisedAutotaskVars, serverCredentialDefaults, hasServerCredentialDefaults } from './config/credentials.js';
|
|
21
21
|
export { ConfigError } from './config/error.js';
|
package/dist/credential.d.ts
CHANGED
|
@@ -1,3 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* credential.ts — the pre-dispatch shape gate for Autotask credential values, and the value-free
|
|
3
|
+
* description of a credential the operator-facing doctor reports.
|
|
4
|
+
*
|
|
5
|
+
* Why this exists (2026-10-07 incident, issues #4/#5/#6): a `.env` file may SINGLE-QUOTE a value on
|
|
6
|
+
* purpose (a secret containing `$` must be, so the shell does not expand it). The shell strips those
|
|
7
|
+
* quotes when the file is loaded with `set -a; . ./.env; set +a`; a quote-PRESERVING loader (most
|
|
8
|
+
* `dotenv` variants, a hand-rolled `split('=')`) hands the value over with its quotes intact. Such a
|
|
9
|
+
* value is printable ASCII, so it passes every transport check, reaches the vendor as
|
|
10
|
+
* `Secret: 'a$b'` and comes back as an opaque 401 that reads like a rotated credential — on an
|
|
11
|
+
* account where repeated auth failures risk a lock.
|
|
12
|
+
*
|
|
13
|
+
* This module closes that at the boundary, on BOTH intake paths, before any client is constructed.
|
|
14
|
+
* It never inspects, stores or reports a credential VALUE — only shape facts derived from it (a
|
|
15
|
+
* length, a two-character prefix, and which anomaly was seen), so every structure here is safe to
|
|
16
|
+
* log and to serialize.
|
|
17
|
+
*
|
|
18
|
+
* The anomaly vocabulary and the anomaly NAMES are this package's own (`detectCredentialShape`), so
|
|
19
|
+
* the envelopes and the doctor report stay stable across SDK versions; the SDK's own validator
|
|
20
|
+
* (`credentialShapeProblem` / `INVALID_CREDENTIAL_SHAPE`) is consumed at construction, where it is
|
|
21
|
+
* part of the SDK's contract with its callers.
|
|
22
|
+
*/
|
|
1
23
|
/**
|
|
2
24
|
* The env-load pitfall every shape rejection points at, without claiming which loader was used.
|
|
3
25
|
* Byte-for-byte the SDK's own pointer (`CREDENTIAL_SHAPE_POINTER` is the same sentence in the SDK's
|
|
@@ -53,12 +75,6 @@ export interface CredentialReport {
|
|
|
53
75
|
export declare function reportCredential(env: Record<string, string | undefined>, name: string): CredentialReport;
|
|
54
76
|
/** The one-line, value-free boot refusal message for a malformed credential variable. */
|
|
55
77
|
export declare function malformedCredentialBootMessage(name: string, anomalies: readonly string[], length: number): string;
|
|
56
|
-
/**
|
|
57
|
-
* The SDK's own verdict on a value, when it has one (`credentialShapeProblem` from node-autotask
|
|
58
|
-
* 0.8.0). Exposed so callers can check that this package's vocabulary and the SDK's agree — and so
|
|
59
|
-
* the companion `INVALID_CREDENTIAL_SHAPE` error the SDK throws at construction can be surfaced as
|
|
60
|
-
* this server's typed `CREDENTIAL_MALFORMED` refusal without re-implementing the predicate.
|
|
61
|
-
*/
|
|
62
78
|
/**
|
|
63
79
|
* The server's OWN refusal text for the SDK loader's `INVALID_CREDENTIAL_SHAPE` verdict (issue #4).
|
|
64
80
|
* Built from the loader's machine code only — the SDK's message embeds a value summary
|
|
@@ -66,4 +82,3 @@ export declare function malformedCredentialBootMessage(name: string, anomalies:
|
|
|
66
82
|
* loader's verdict, and the load recipe; no length, no prefix, no character of any value.
|
|
67
83
|
*/
|
|
68
84
|
export declare const MALFORMED_CREDENTIAL_REFUSAL: string;
|
|
69
|
-
export declare function sdkShapeProblem(value: string): string | undefined;
|
package/dist/credential.js
CHANGED
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
* (`credentialShapeProblem` / `INVALID_CREDENTIAL_SHAPE`) is consumed at construction, where it is
|
|
21
21
|
* part of the SDK's contract with its callers.
|
|
22
22
|
*/
|
|
23
|
-
import { credentialShapeProblem } from 'node-autotask';
|
|
24
23
|
/**
|
|
25
24
|
* The env-load pitfall every shape rejection points at, without claiming which loader was used.
|
|
26
25
|
* Byte-for-byte the SDK's own pointer (`CREDENTIAL_SHAPE_POINTER` is the same sentence in the SDK's
|
|
@@ -104,12 +103,6 @@ export function malformedCredentialBootMessage(name, anomalies, length) {
|
|
|
104
103
|
return (`${name} holds a value whose shape cannot be right (length ${length}, ${anomalies.join(', ')}); ` +
|
|
105
104
|
`this server refuses to boot rather than present a malformed credential to Autotask (issue #4). ${CREDENTIAL_LOAD_HINT}`);
|
|
106
105
|
}
|
|
107
|
-
/**
|
|
108
|
-
* The SDK's own verdict on a value, when it has one (`credentialShapeProblem` from node-autotask
|
|
109
|
-
* 0.8.0). Exposed so callers can check that this package's vocabulary and the SDK's agree — and so
|
|
110
|
-
* the companion `INVALID_CREDENTIAL_SHAPE` error the SDK throws at construction can be surfaced as
|
|
111
|
-
* this server's typed `CREDENTIAL_MALFORMED` refusal without re-implementing the predicate.
|
|
112
|
-
*/
|
|
113
106
|
/**
|
|
114
107
|
* The server's OWN refusal text for the SDK loader's `INVALID_CREDENTIAL_SHAPE` verdict (issue #4).
|
|
115
108
|
* Built from the loader's machine code only — the SDK's message embeds a value summary
|
|
@@ -119,7 +112,4 @@ export function malformedCredentialBootMessage(name, anomalies, length) {
|
|
|
119
112
|
export const MALFORMED_CREDENTIAL_REFUSAL = 'one or more credential values were refused by the SDK\'s canonical loader as a malformed shape; ' +
|
|
120
113
|
'no request was sent. Fix the value in the environment this server reads and reload it. ' +
|
|
121
114
|
CREDENTIAL_LOAD_HINT;
|
|
122
|
-
export function sdkShapeProblem(value) {
|
|
123
|
-
return credentialShapeProblem(value);
|
|
124
|
-
}
|
|
125
115
|
//# sourceMappingURL=credential.js.map
|
package/dist/main.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* only decision this file makes.
|
|
11
11
|
*/
|
|
12
12
|
import { createMcpHandler } from '@modelcontextprotocol/server';
|
|
13
|
-
import { ConfigError, loadConfig, serverCredentialDefaults } from './config.js';
|
|
13
|
+
import { ConfigError, loadConfig, serverCredentialDefaults, unrecognisedAutotaskVars } from './config.js';
|
|
14
14
|
import { runMoves } from './moves.js';
|
|
15
15
|
import { runDoctor } from './doctor.js';
|
|
16
16
|
import { createLogger } from './logger.js';
|
|
@@ -166,6 +166,10 @@ function main() {
|
|
|
166
166
|
// the logs must stay on stderr.
|
|
167
167
|
const logSink = config.MCP_TRANSPORT === 'http' ? process.stdout : process.stderr;
|
|
168
168
|
const log = createLogger(config.LOG_LEVEL, (line) => logSink.write(line + '\n'));
|
|
169
|
+
// Stale or mistyped AUTOTASK_* names are ignored, never a boot failure: say so once.
|
|
170
|
+
const unrecognised = unrecognisedAutotaskVars(env);
|
|
171
|
+
if (unrecognised.length > 0)
|
|
172
|
+
log.warn('ignoring unrecognised AUTOTASK_* variables (removed in an earlier version, or misspelled)', { variables: unrecognised });
|
|
169
173
|
// Security V9 (FT-3): the lab-only allow-any opt-in is a security-posture warning for the
|
|
170
174
|
// operator. It is NOT part of the operational NDJSON stream (under http that is stdout), so
|
|
171
175
|
// it goes straight to stderr on every transport — the stream an operator/monitor reads for
|
|
@@ -96,7 +96,7 @@ export type AutotaskCacheOptions = {
|
|
|
96
96
|
resourceNameCache?: false;
|
|
97
97
|
};
|
|
98
98
|
/**
|
|
99
|
-
* The SDK cache options the preset selects (
|
|
99
|
+
* The SDK cache options the preset selects (approved cache deviation, docs/architecture.md 8.1):
|
|
100
100
|
* `on` sends NO cache blocks — the SDK's default-on caches apply to every pooled tenant client
|
|
101
101
|
* (read ~5 min + the plan-derived reference table, field ~5 min, resource-name ~1 h; in-process,
|
|
102
102
|
* per-tenant, TTL-bounded, never cross-tenant, never on disk; the server holds no TTL value of
|
|
@@ -147,7 +147,7 @@ export function toolDescription(tool) {
|
|
|
147
147
|
return text;
|
|
148
148
|
}
|
|
149
149
|
/**
|
|
150
|
-
* The SDK cache options the preset selects (
|
|
150
|
+
* The SDK cache options the preset selects (approved cache deviation, docs/architecture.md 8.1):
|
|
151
151
|
* `on` sends NO cache blocks — the SDK's default-on caches apply to every pooled tenant client
|
|
152
152
|
* (read ~5 min + the plan-derived reference table, field ~5 min, resource-name ~1 h; in-process,
|
|
153
153
|
* per-tenant, TTL-bounded, never cross-tenant, never on disk; the server holds no TTL value of
|
package/dist/tools/composites.js
CHANGED
|
@@ -26,7 +26,7 @@ import { AutotaskConfigError, NotFoundError } from 'node-autotask';
|
|
|
26
26
|
import { TICKET_FULL_CONTEXT_INPUT, COMPANY_FULL_PROFILE_INPUT, COMPANY_PROFILE_PART_LIMIT_DEFAULT, CONFIGURATION_ITEM_PROFILE_INPUT, PROJECT_GET_CONTEXT_INPUT, QUOTE_GET_INPUT, INVOICE_GET_INPUT, CONTACT_FULL_PROFILE_INPUT, SERVICE_CALL_FULL_PROFILE_INPUT, CONTRACT_FULL_PROFILE_INPUT, RESOURCE_WORKLOAD_INPUT, closedInput, } from './schemas.js';
|
|
27
27
|
import { abortedAnswer } from './dispatch.js';
|
|
28
28
|
import { crossResourceUntrusted, documentCore, markFilePayload, markResult, ok } from './results.js';
|
|
29
|
-
import { resolveOutputTarget,
|
|
29
|
+
import { resolveOutputTarget, writeOutput } from './output-file.js';
|
|
30
30
|
import { assertOperationPermitted } from '../policy.js';
|
|
31
31
|
/** The cap on a fan-out `in` filter (the task-id join between two parts). */
|
|
32
32
|
const IN_FILTER_CAP = 100;
|
|
@@ -265,7 +265,7 @@ export function buildInvoiceGet(ctx) {
|
|
|
265
265
|
// that arrives pre-wrapped is a vendor forgery — the pinned SDK's read path returns raw).
|
|
266
266
|
const doc = documentCore(result);
|
|
267
267
|
if (target !== undefined) {
|
|
268
|
-
const bytes = await
|
|
268
|
+
const bytes = await writeOutput(target, doc);
|
|
269
269
|
return ok(`Invoice ${args.format} export written to the configured output directory (${bytes} bytes); the file holds the raw document, the result carries only the path and size.`, { format: args.format, result: { outputPath: target.path, bytes, encoding: 'utf-8' } });
|
|
270
270
|
}
|
|
271
271
|
const buf = Buffer.from(doc, 'utf8');
|
|
@@ -276,7 +276,7 @@ export function buildInvoiceGet(ctx) {
|
|
|
276
276
|
// pdf: the SDK's file payload { encoding, byteLength, truncated, data? }.
|
|
277
277
|
const payload = markResult(result, ctx.untrustedFields(op));
|
|
278
278
|
if (target !== undefined && payload.truncated !== true && typeof payload.data === 'string') {
|
|
279
|
-
const bytes = await
|
|
279
|
+
const bytes = await writeOutput(target, Buffer.from(payload.data, 'base64'));
|
|
280
280
|
return ok(`Invoice ${args.format} export written to the configured output directory (${bytes} bytes); the file holds the raw document, the result carries only the path and size.`, { format: args.format, result: { outputPath: target.path, bytes, encoding: 'binary' } });
|
|
281
281
|
}
|
|
282
282
|
markFilePayload(payload);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { AutotaskConfig } from 'node-autotask';
|
|
2
|
+
import { type ClientPool } from 'node-autotask/pool';
|
|
2
3
|
import type { Config } from '../../config.js';
|
|
3
4
|
import type { Logger } from '../../logger.js';
|
|
4
5
|
import type { ToolClient } from './types.js';
|
|
@@ -21,3 +22,5 @@ export declare function buildStdioClient(config: Config, log: Logger): {
|
|
|
21
22
|
client: ToolClient;
|
|
22
23
|
origin: string;
|
|
23
24
|
};
|
|
25
|
+
/** The process-lifetime bounded per-tenant client pool of the http transport (security D-1, ratified). */
|
|
26
|
+
export declare function createTenantPool(log: Logger): ClientPool;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** SDK client construction: the base options, the placeholder client and the stdio client (issue #91). */
|
|
2
2
|
import { AutotaskClient } from 'node-autotask';
|
|
3
|
+
import { createClientPool } from 'node-autotask/pool';
|
|
3
4
|
import { serverCredentialDefaults } from '../../config.js';
|
|
4
5
|
import { normalizeBaseUrl } from '../../base-hosts.js';
|
|
5
6
|
import { cacheOptionsForPreset } from '../catalog-data.js';
|
|
@@ -34,7 +35,7 @@ export function buildSdkBaseOptions(config, log) {
|
|
|
34
35
|
blockPrivateHosts: true,
|
|
35
36
|
...(defaultBaseUrl !== undefined ? { allowedOrigins: [defaultBaseUrl] } : {}),
|
|
36
37
|
},
|
|
37
|
-
// The
|
|
38
|
+
// The approved cache deviation, docs/architecture.md 8.1 (user-approved): `on` sends no cache blocks (the SDK
|
|
38
39
|
// default-on caches) UNLESS the AUTOTASK_FIELD_CACHE_TTL_MS override is set — then the
|
|
39
40
|
// scalar fieldCache block rides (the TTL pass-through); `off` opts every channel out
|
|
40
41
|
// explicitly (every read re-dials the vendor) and is never touched by the override.
|
|
@@ -93,4 +94,10 @@ export function buildStdioClient(config, log) {
|
|
|
93
94
|
});
|
|
94
95
|
return { client, origin: normalized.origin };
|
|
95
96
|
}
|
|
97
|
+
/** The process-lifetime bounded per-tenant client pool of the http transport (security D-1, ratified). */
|
|
98
|
+
export function createTenantPool(log) {
|
|
99
|
+
const pool = createClientPool({ maxSize: 128 });
|
|
100
|
+
log.info('tenant client pool ready', { maxSize: 128 });
|
|
101
|
+
return pool;
|
|
102
|
+
}
|
|
96
103
|
//# sourceMappingURL=client.js.map
|
package/dist/tools/context.d.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* the pool + the credential-tuple config, so a vendor 401 deletes the entry (no negative
|
|
16
16
|
* caching; security S1/C1.8, S3/C3.5).
|
|
17
17
|
*
|
|
18
|
-
* Client state (
|
|
18
|
+
* Client state (approved cache deviation, docs/architecture.md 8.1): under `http` one BOUNDED in-process
|
|
19
19
|
* LRU pool (`node-autotask/pool`, default capacity 128) of tenant clients, keyed by the SDK's
|
|
20
20
|
* SHA-256 of the NORMALISED credential tuple (user, integration code, secret, resolved base URL,
|
|
21
21
|
* impersonation id) — raw credentials never appear in keys, logs or eviction messages. Each
|
|
@@ -33,6 +33,5 @@
|
|
|
33
33
|
export type { ToolClient, HourlyBudget, QueryWalkParams, QueryWalkResult, ToolOps, ToolDeps, WriteDeps, ToolContext, ResolvedRequestScope, HttpScopeResolver } from './context/types.js';
|
|
34
34
|
export { defaultToolOps } from './context/ops.js';
|
|
35
35
|
export { deriveToolContext, releaseClientOnError } from './context/derive.js';
|
|
36
|
-
export { buildSdkBaseOptions, buildStdioClient } from './context/client.js';
|
|
36
|
+
export { buildSdkBaseOptions, buildStdioClient, createTenantPool } from './context/client.js';
|
|
37
37
|
export { createHttpScopeResolver } from './context/scope.js';
|
|
38
|
-
export { createTenantPool } from './context/pool.js';
|
package/dist/tools/context.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { defaultToolOps } from './context/ops.js';
|
|
2
2
|
export { deriveToolContext, releaseClientOnError } from './context/derive.js';
|
|
3
|
-
export { buildSdkBaseOptions, buildStdioClient } from './context/client.js';
|
|
3
|
+
export { buildSdkBaseOptions, buildStdioClient, createTenantPool } from './context/client.js';
|
|
4
4
|
export { createHttpScopeResolver } from './context/scope.js';
|
|
5
|
-
export { createTenantPool } from './context/pool.js';
|
|
6
5
|
//# sourceMappingURL=context.js.map
|
package/dist/tools/dispatch.js
CHANGED
|
@@ -260,24 +260,21 @@ export async function readOneItem(ctx, operation, args, signal, opts) {
|
|
|
260
260
|
// path and this bounded default is its contract; autotask_query refuses the missing filter because it is the guided surface.
|
|
261
261
|
if (operation.endsWith('.query') && cleanArgs[0] === undefined)
|
|
262
262
|
cleanArgs[0] = { limit: QUERY_LIMIT.parse(undefined) };
|
|
263
|
-
|
|
264
|
-
//
|
|
265
|
-
// limit
|
|
266
|
-
|
|
267
|
-
cleanArgs[0]
|
|
263
|
+
// #173: a digit-string limit or latest (a client that writes every value as text, e.g. Copilot Studio's
|
|
264
|
+
// limit: "5") is that number; the SDK still range-checks it (it refuses a string `latest`, so the
|
|
265
|
+
// default limit and the scope guard below must see the number).
|
|
266
|
+
for (const key of ['limit', 'latest']) {
|
|
267
|
+
const given = cleanArgs[0];
|
|
268
|
+
const v = given?.[key];
|
|
269
|
+
if (given !== null && typeof given === 'object' && !Array.isArray(given) && typeof v === 'string' && /^\d{1,9}$/.test(v))
|
|
270
|
+
cleanArgs[0] = { ...given, [key]: Number(v) };
|
|
268
271
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (given2 !== null && typeof given2 === 'object' && !Array.isArray(given2) && typeof given2.latest === 'string' && /^\d{1,9}$/.test(given2.latest)) {
|
|
273
|
-
cleanArgs[0] = { ...given2, latest: Number(given2.latest) };
|
|
274
|
-
}
|
|
275
|
-
const given3 = cleanArgs[0];
|
|
276
|
-
if (given3 !== null && typeof given3 === 'object' && !Array.isArray(given3)) {
|
|
277
|
-
assertFilterShape(given3.filter, operation);
|
|
272
|
+
const bagArg = cleanArgs[0];
|
|
273
|
+
if (bagArg !== null && typeof bagArg === 'object' && !Array.isArray(bagArg)) {
|
|
274
|
+
assertFilterShape(bagArg.filter, operation);
|
|
278
275
|
// #234: ticket.<field> on tickets (task., project. likewise) is the own field
|
|
279
276
|
const resource = operation.slice(0, operation.lastIndexOf('.'));
|
|
280
|
-
const bag = { ...
|
|
277
|
+
const bag = { ...bagArg };
|
|
281
278
|
if (bag.filter !== undefined)
|
|
282
279
|
bag.filter = normalizeFilter(bag.filter, resource);
|
|
283
280
|
if (Array.isArray(bag.includeFields))
|
|
@@ -319,6 +316,12 @@ export async function readOneItem(ctx, operation, args, signal, opts) {
|
|
|
319
316
|
if (verdict.aborted)
|
|
320
317
|
return { aborted: true, result: undefined };
|
|
321
318
|
guardNote = verdict.note;
|
|
319
|
+
// #245: a no-filter walk answers the first rows of the tenant table; say so, so the page is not read as the full answer
|
|
320
|
+
const f = sent.filter;
|
|
321
|
+
if ((f === undefined || (Array.isArray(f) && f.length === 0)) && sent.latest === undefined && typeof sent.limit === 'number') {
|
|
322
|
+
const noFilter = `no filter: the first ${sent.limit} rows in id order, not a scoped or complete answer.`;
|
|
323
|
+
guardNote = guardNote === undefined ? noFilter : `${guardNote} ${noFilter}`;
|
|
324
|
+
}
|
|
322
325
|
}
|
|
323
326
|
if (operation === 'contracts.profitability') {
|
|
324
327
|
const rawBag = cleanArgs[1];
|