iit-mcp-autotask 0.35.0 → 0.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/capabilities.json +2 -2
- package/dist/tools/catalog-data.d.ts +5 -5
- package/dist/tools/catalog-data.js +8 -8
- package/dist/tools/dispatch.d.ts +7 -0
- package/dist/tools/dispatch.js +64 -2
- package/dist/tools/field-pointer.d.ts +2 -2
- package/dist/tools/field-pointer.js +8 -4
- package/dist/tools/meta.d.ts +6 -0
- package/dist/tools/meta.js +42 -26
- package/dist/tools/provenance-scrub.d.ts +20 -0
- package/dist/tools/provenance-scrub.js +209 -0
- package/dist/tools/reasons.d.ts +2 -1
- package/dist/tools/reasons.js +10 -0
- package/dist/tools/resolves.d.ts +8 -0
- package/dist/tools/resolves.js +16 -1
- package/dist/tools/results/errors.js +2 -0
- package/dist/tools/results/untrusted.d.ts +10 -5
- package/dist/tools/results/untrusted.js +7 -3
- package/dist/tools/schemas.d.ts +4 -1
- package/dist/tools/schemas.js +2 -2
- package/dist/tools/server-version.d.ts +1 -1
- package/dist/tools/server-version.js +1 -1
- package/dist/tools/server.d.ts +1 -1
- package/dist/tools/write/checks.d.ts +2 -0
- package/dist/tools/write/checks.js +1 -0
- package/dist/tools/write/facts.d.ts +9 -5
- package/dist/tools/write/facts.js +42 -20
- package/dist/tools/write/preview.js +5 -5
- package/dist/tools/write-guidance.d.ts +9 -18
- package/dist/tools/write-guidance.js +39 -29
- package/package.json +2 -2
- package/server.json +2 -2
- package/skills/filter-semantics.md +1 -1
package/README.md
CHANGED
|
@@ -8,10 +8,10 @@ context into one call, a generic read layer for the long tail, one result envelo
|
|
|
8
8
|
packaged playbooks, strong security boundaries, and predictable per-tenant in-process state (pooled
|
|
9
9
|
client + SDK-owned TTL caches).
|
|
10
10
|
|
|
11
|
-
> **Package status (2026-10-
|
|
12
|
-
> at `0.
|
|
11
|
+
> **Package status (2026-10-29):** the runtime dependency `node-autotask` is published to public npm
|
|
12
|
+
> at `0.37.0`; the committed `pnpm-lock.yaml` pins that
|
|
13
13
|
> release with its integrity hash, so installs resolve deterministically. The dependency is always npm
|
|
14
|
-
> (node-autotask pinned exactly at `0.
|
|
14
|
+
> (node-autotask pinned exactly at `0.37.0`) — never a `file:`, `git:`, `link:` or workspace reference
|
|
15
15
|
> to a local checkout.
|
|
16
16
|
|
|
17
17
|
> **0.6.0 is a breaking release** (unreleased): tool names, result shapes and configuration all
|
|
@@ -21,7 +21,7 @@ client + SDK-owned TTL caches).
|
|
|
21
21
|
|
|
22
22
|
The **core** toolset (12 tools) is always served; `AUTOTASK_TOOLSETS` adds `service`, `records` and
|
|
23
23
|
`finance` (or `all`, 19 tools); the operator profile adds two. The whole tool surface (21 tools) is
|
|
24
|
-
listed below. Every other operation in the SDK's 1,
|
|
24
|
+
listed below. Every other operation in the SDK's 1,281-operation registry stays
|
|
25
25
|
reachable through the generic layer: `autotask_catalog` finds the key, `autotask_describe` serves
|
|
26
26
|
its contract, `autotask_read` runs it.
|
|
27
27
|
|
|
@@ -763,7 +763,7 @@ independent review with its resolutions, [`docs/reviews/0.6.0.md`](docs/reviews/
|
|
|
763
763
|
|
|
764
764
|
## SDK dependency
|
|
765
765
|
|
|
766
|
-
`iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.
|
|
766
|
+
`iit-mcp-autotask` depends on **`node-autotask` from npm at exactly `0.37.0`** (an exact pin —
|
|
767
767
|
no range operator; subpaths `.` / `/capabilities` / `/operations` / `/resources` / `/types` /
|
|
768
768
|
`/pool`) — the only **vendor** dependency; the other two runtime dependencies are
|
|
769
769
|
`@modelcontextprotocol/server` (the MCP framework) and `zod` (schema validation). It never
|
|
@@ -773,7 +773,7 @@ user — the published tarball ships no lockfile) must resolve to the release th
|
|
|
773
773
|
reviewed against, or the plan-hash boot check refuses to start.
|
|
774
774
|
|
|
775
775
|
**Publishing `node-autotask` is the maintainer's action, done manually.** It is published on the
|
|
776
|
-
public registry at `0.
|
|
776
|
+
public registry at `0.37.0` (published 2026-10-28 UTC); the
|
|
777
777
|
exact pin and the committed `pnpm-lock.yaml` (with `pnpm-workspace.yaml`) pin the real published
|
|
778
778
|
tarball with its integrity hash, so installs resolve to exactly that release — the lockfile
|
|
779
779
|
freezes the pnpm/git/Docker paths, the pin freezes the npm/npx paths. The docs are written against this
|
|
@@ -867,9 +867,9 @@ physical assets (Autotask models them as Configuration Items — covered).
|
|
|
867
867
|
|
|
868
868
|
| # | Item | Status |
|
|
869
869
|
|---|------|--------|
|
|
870
|
-
| B1 | **Project phase & task lists** (W7 sub-capability): no `project_tasks`/`project_phases` operations in the SDK's 1,
|
|
870
|
+
| B1 | **Project phase & task lists** (W7 sub-capability): no `project_tasks`/`project_phases` operations in the SDK's 1,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 |
|
|
871
871
|
| B2 | **Time-entry writes on ww29/australia-nz for API-only users**: vendor 500 (live-verified). Time-entry creation works where the API user is a real resource, through the operator profile (`log-time-entry`); on ww29 the write is classified `failed` (a 500 with a vendor `errors[]` body), never `unknown`. | zone limitation |
|
|
872
|
-
| 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. Reachable via the generic layer. | resolved in SDK 0.32.0 (not re-verified live) |
|
|
872
|
+
| 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) |
|
|
873
873
|
| B4 | **CI create on tenants without products** (ww29 product wall: `products.count = 0`): `configuration_items.create` requires an existing `productID`. No dedicated CI-create in v1; the wall is documented on the generic side. | zone limitation |
|
|
874
874
|
| 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 |
|
|
875
875
|
| 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 |
|
package/capabilities.json
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
},
|
|
70
70
|
{
|
|
71
71
|
"id": "autotask.platform.catalog",
|
|
72
|
-
"description": "Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,
|
|
72
|
+
"description": "Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,281 registry operations, with effect and whether this deployment serves it. No vendor call.",
|
|
73
73
|
"mcpPrimitive": "tool",
|
|
74
74
|
"toolName": "autotask_catalog",
|
|
75
75
|
"type": "read",
|
|
@@ -395,7 +395,7 @@
|
|
|
395
395
|
"autotask_describe",
|
|
396
396
|
"autotask_read"
|
|
397
397
|
],
|
|
398
|
-
"backingRegistryOperations":
|
|
398
|
+
"backingRegistryOperations": 1281,
|
|
399
399
|
"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."
|
|
400
400
|
},
|
|
401
401
|
"schemas": {}
|
|
@@ -65,10 +65,10 @@ export declare const UNTRUSTED_NOTE = "Externally-sourced free text in the resul
|
|
|
65
65
|
/** The record's plan-derived untrusted field list (the registry stamp). */
|
|
66
66
|
export declare function untrustedFieldsOf(rec: CapabilityRecord): string[];
|
|
67
67
|
/**
|
|
68
|
-
* The resources that carry user-defined fields (#183 S8),
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
68
|
+
* The resources that carry user-defined fields (#183 S8), read from the registry with zero wire:
|
|
69
|
+
* each record's published `udfCapable` flag (node-autotask 0.37.0 #254; the plan's entities whose SDK
|
|
70
|
+
* resource class normalises `userDefinedFields`). Not the `getUdfInfo` set (every vendor entity has
|
|
71
|
+
* that metadata read, UDFs or not). A test pins the exact set.
|
|
72
72
|
*/
|
|
73
73
|
export declare const UDF_CAPABLE: ReadonlySet<string>;
|
|
74
74
|
/** The per-tool description budget (A0 slimming: every served description <= 250 chars). */
|
|
@@ -109,7 +109,7 @@ export declare function cacheOptionsForPreset(preset: 'on' | 'off', fieldCacheTt
|
|
|
109
109
|
* fails the boot (or the CI test) instead of quietly shipping a tool surface that no longer
|
|
110
110
|
* describes the SDK behind it.
|
|
111
111
|
*/
|
|
112
|
-
export declare const BUILT_AGAINST_PLAN_HASH = "
|
|
112
|
+
export declare const BUILT_AGAINST_PLAN_HASH = "5cd6c6679384651e7f67a0135915bcc031ef542640d9ebb6992b729ca42efe37";
|
|
113
113
|
/**
|
|
114
114
|
* Fail fast when the installed SDK's plan has moved out from under this surface.
|
|
115
115
|
* Called at boot (main.ts) and safe to call repeatedly (pure comparison).
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Two sources, both shipped inside the `node-autotask` package:
|
|
5
5
|
* - `MCP_TOOL_CATALOG.json` — the machine catalog: one row per registry operation (projected
|
|
6
6
|
* tool, effect, dry-run/confirm, required args, tier, reach) and the plan hash;
|
|
7
|
-
* - the compiled capability registry (`node-autotask/capabilities`) — the 1,
|
|
7
|
+
* - the compiled capability registry (`node-autotask/capabilities`) — the 1,281-operation
|
|
8
8
|
* records the SDK's own invoke governor resolves against.
|
|
9
9
|
*
|
|
10
10
|
* The JSON file is NOT behind the package's `exports` map, so it is read from the installed
|
|
@@ -102,12 +102,12 @@ export function untrustedFieldsOf(rec) {
|
|
|
102
102
|
return Array.isArray(list) ? list : [];
|
|
103
103
|
}
|
|
104
104
|
/**
|
|
105
|
-
* The resources that carry user-defined fields (#183 S8),
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
105
|
+
* The resources that carry user-defined fields (#183 S8), read from the registry with zero wire:
|
|
106
|
+
* each record's published `udfCapable` flag (node-autotask 0.37.0 #254; the plan's entities whose SDK
|
|
107
|
+
* resource class normalises `userDefinedFields`). Not the `getUdfInfo` set (every vendor entity has
|
|
108
|
+
* that metadata read, UDFs or not). A test pins the exact set.
|
|
109
109
|
*/
|
|
110
|
-
export const UDF_CAPABLE = new Set(RECORDS.filter((r) =>
|
|
110
|
+
export const UDF_CAPABLE = new Set(RECORDS.filter((r) => r.udfCapable).map((r) => r.resource));
|
|
111
111
|
/** The per-tool description budget (A0 slimming: every served description <= 250 chars). */
|
|
112
112
|
export const MAX_DESCRIPTION_CHARS = 250;
|
|
113
113
|
/**
|
|
@@ -117,7 +117,7 @@ export const MAX_DESCRIPTION_CHARS = 250;
|
|
|
117
117
|
*/
|
|
118
118
|
export const TOOL_DESCRIPTIONS = {
|
|
119
119
|
autotask_guide: 'Step-by-step playbooks for multi-step Autotask tasks. No args: the index; {skill} returns one playbook, {skill, section} one section. Static text, no vendor call. Use it first when a task needs several tools.',
|
|
120
|
-
autotask_catalog: 'Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,
|
|
120
|
+
autotask_catalog: 'Find the exact operation key for anything without a dedicated tool: ranked search (q) over the 1,281 registry operations, with effect and whether this deployment serves it. No vendor call.',
|
|
121
121
|
autotask_describe: 'One operation\'s (registry key or tool name) exact input/output contract, examples, errors, policy verdict and playbooks (operation), or one resource\'s relationship map: parent, children, lookup keys (resource). No vendor call.',
|
|
122
122
|
autotask_read: 'Run reads by exact registry key, or up to 10 in one call: unbilled work, profitability and margin, SLA health. *.query: 50 rows; scope by company/contract ids (unscoped = tenant walk); roll-ups: autotask_aggregate; slim: includeFields, noteChars.',
|
|
123
123
|
autotask_resolve: 'Find the record behind an identifier: id or name, ticket number, CI serial, contact email (by), staff by email, user name or name (resource resources, include_api_users); one resource or a sweep. Exact by default; contains: companies, tickets, sweep.',
|
|
@@ -173,7 +173,7 @@ export function cacheOptionsForPreset(preset, fieldCacheTtlMs) {
|
|
|
173
173
|
* fails the boot (or the CI test) instead of quietly shipping a tool surface that no longer
|
|
174
174
|
* describes the SDK behind it.
|
|
175
175
|
*/
|
|
176
|
-
export const BUILT_AGAINST_PLAN_HASH = '
|
|
176
|
+
export const BUILT_AGAINST_PLAN_HASH = '5cd6c6679384651e7f67a0135915bcc031ef542640d9ebb6992b729ca42efe37';
|
|
177
177
|
/**
|
|
178
178
|
* Fail fast when the installed SDK's plan has moved out from under this surface.
|
|
179
179
|
* Called at boot (main.ts) and safe to call repeatedly (pure comparison).
|
package/dist/tools/dispatch.d.ts
CHANGED
|
@@ -56,6 +56,13 @@ export declare function abortedAnswer(corr: string): ToolReturn;
|
|
|
56
56
|
* in a 4xx body; the error wrap value-scrubs them before the envelope crosses.
|
|
57
57
|
*/
|
|
58
58
|
export declare function requestSecrets(ctx: ToolContext): string[];
|
|
59
|
+
/**
|
|
60
|
+
* A vendor TimeEntries continuation URL (#224; mirrors the SDK's cursorToPath + entryCursorPath, tighter):
|
|
61
|
+
* printable ASCII with no whitespace, absolute and already in canonical form, on this request's own Autotask origin, no userinfo, no
|
|
62
|
+
* fragment, exactly the continuation path, and only `paging` / `search` query parameters with encoded
|
|
63
|
+
* values. Anything else is not granted the cursor exemption (it is served as wrapped, capped vendor text).
|
|
64
|
+
*/
|
|
65
|
+
export declare function isEntryCursor(value: string, origin: string | undefined): boolean;
|
|
59
66
|
/**
|
|
60
67
|
* Per-operation post-processing for results read through `autotask_read`: the helper operations
|
|
61
68
|
* that used to have their own tools carry vendor text beyond the registry stamp, and this is where
|
package/dist/tools/dispatch.js
CHANGED
|
@@ -15,8 +15,9 @@ import { AutotaskConfigError } from 'node-autotask';
|
|
|
15
15
|
import { stripUntrustedDeep } from 'node-autotask/untrusted';
|
|
16
16
|
import { assertFilterShape, QUERY_LIMIT, QUERY_RESOURCES } from './schemas.js';
|
|
17
17
|
import { recordById, untrustedFieldsOf } from './catalog-data.js';
|
|
18
|
+
import { AUTOTASK_BASE_PATH } from '../base-hosts.js';
|
|
18
19
|
import { releaseClientOnError } from './context.js';
|
|
19
|
-
import { abortedResult, closedArgsError, crossResourceUntrusted, errorContent, isFileOperation, markFilePayload, markResult, outcome, refusalContent } from './results.js';
|
|
20
|
+
import { abortedResult, closedArgsError, crossResourceUntrusted, errorContent, isFileOperation, markFilePayload, markResult, markServerField, outcome, refusalContent } from './results.js';
|
|
20
21
|
import { lastFullCalendarMonth, lastFullCalendarMonthLabel } from './profitability-window.js';
|
|
21
22
|
import { assertOperationPermitted } from '../policy.js';
|
|
22
23
|
import { ACK_KEY, guardUnscopedWalk, ScopeRequiredError } from './scope-guard.js';
|
|
@@ -83,7 +84,7 @@ async function frame(ctx, input, rawArgs, extra, work) {
|
|
|
83
84
|
if (!parsed.success) {
|
|
84
85
|
const issue = parsed.error.issues[0];
|
|
85
86
|
const field = issue === undefined ? undefined : issue.path.map(String).join('.');
|
|
86
|
-
return errorContent(new InputInvalidError(`invalid input: ${parsed.error.issues.map((i) => `${i.path.map(String).join('.') || '(input)'}: ${i.message}`).join('; ')}`, field === '' ? undefined : field), ctx.correlationId);
|
|
87
|
+
return errorContent(new InputInvalidError(`invalid input: ${input.hint?.(raw) ?? ''}${parsed.error.issues.map((i) => `${i.path.map(String).join('.') || '(input)'}: ${i.message}`).join('; ')}`, field === '' ? undefined : field), ctx.correlationId);
|
|
87
88
|
}
|
|
88
89
|
if (ctx.refusal !== undefined)
|
|
89
90
|
return refusalContent(ctx.refusal.kind, ctx.correlationId, ctx.refusal.anomalies ?? []);
|
|
@@ -143,6 +144,35 @@ export function requestSecrets(ctx) {
|
|
|
143
144
|
}
|
|
144
145
|
return out;
|
|
145
146
|
}
|
|
147
|
+
/** The time_entries posted-state reads whose `nextCursor` is a pass-back pagination cursor (SDK 0.37.0 #242). */
|
|
148
|
+
const CURSOR_OPS = new Set(['time_entries.getPosted', 'time_entries.getUnposted', 'time_entries.postedWithin', 'time_entries.dateWorkedWindow']);
|
|
149
|
+
/** The continuation path a TimeEntries cursor names, under the fixed base path (compared case-insensitively). */
|
|
150
|
+
const ENTRY_CURSOR_PATH = `${AUTOTASK_BASE_PATH}/V1.0/TimeEntries/query/next`.toLowerCase();
|
|
151
|
+
/** The query a vendor continuation URL carries: `paging` and `search`, URL-encoded values only. */
|
|
152
|
+
const ENTRY_CURSOR_PARAM = /^(?:paging|search)=[A-Za-z0-9%._~+-]+$/;
|
|
153
|
+
/**
|
|
154
|
+
* A vendor TimeEntries continuation URL (#224; mirrors the SDK's cursorToPath + entryCursorPath, tighter):
|
|
155
|
+
* printable ASCII with no whitespace, absolute and already in canonical form, on this request's own Autotask origin, no userinfo, no
|
|
156
|
+
* fragment, exactly the continuation path, and only `paging` / `search` query parameters with encoded
|
|
157
|
+
* values. Anything else is not granted the cursor exemption (it is served as wrapped, capped vendor text).
|
|
158
|
+
*/
|
|
159
|
+
export function isEntryCursor(value, origin) {
|
|
160
|
+
if (origin === undefined || !/^[\x21-\x7e]+$/.test(value) || !value.startsWith(`${origin}/`))
|
|
161
|
+
return false;
|
|
162
|
+
let u;
|
|
163
|
+
try {
|
|
164
|
+
u = new URL(value);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
if (u.href !== value || u.origin !== origin || u.username !== '' || u.password !== '' || u.hash !== '' || value.includes('#'))
|
|
170
|
+
return false;
|
|
171
|
+
if (u.pathname.toLowerCase() !== ENTRY_CURSOR_PATH)
|
|
172
|
+
return false;
|
|
173
|
+
const query = u.search.replace(/^\?/, '');
|
|
174
|
+
return query === '' || query.split('&').every((p) => ENTRY_CURSOR_PARAM.test(p));
|
|
175
|
+
}
|
|
146
176
|
/** The reads behind the profitability result's customer and part containers (their untrusted stamps). */
|
|
147
177
|
const PROFITABILITY_PART_OPS = [
|
|
148
178
|
'contracts.profitability',
|
|
@@ -179,7 +209,30 @@ const PROFITABILITY_UNTRUSTED = [
|
|
|
179
209
|
*/
|
|
180
210
|
export const READ_POSTPROCESS = {
|
|
181
211
|
'contracts.profitability': (result) => markResult(result, PROFITABILITY_UNTRUSTED),
|
|
212
|
+
'operations.revenueRollup': markRevenueRollup,
|
|
182
213
|
};
|
|
214
|
+
/**
|
|
215
|
+
* `operations.revenueRollup` (SDK 0.37.0): the helper rows carry no `metadata.untrustedFields`. Vendor text
|
|
216
|
+
* sits in `rows[].keys.<dimension>` (`name`, `division`, `label`) and in the values of
|
|
217
|
+
* `coverage.contractTypeMap` / `contractCategoryMap` (id -> picklist label). The cross-resource list
|
|
218
|
+
* covers `name` and `label` at every depth; `division` (the LOB level-1 name) is added. ponytail: the
|
|
219
|
+
* `coverage.excluded.otherContract*` maps are keyed by label, and a key cannot be wrapped (it is still
|
|
220
|
+
* cleaned and capped by the output sanitiser).
|
|
221
|
+
*/
|
|
222
|
+
const ROLLUP_UNTRUSTED = [...crossResourceUntrusted(), 'division'];
|
|
223
|
+
function markRevenueRollup(result) {
|
|
224
|
+
const out = markResult(result, ROLLUP_UNTRUSTED);
|
|
225
|
+
const cov = out?.coverage;
|
|
226
|
+
for (const key of ['contractTypeMap', 'contractCategoryMap']) {
|
|
227
|
+
const map = cov?.[key];
|
|
228
|
+
if (map === null || typeof map !== 'object' || Array.isArray(map))
|
|
229
|
+
continue;
|
|
230
|
+
for (const [id, label] of Object.entries(map))
|
|
231
|
+
if (typeof label === 'string')
|
|
232
|
+
markServerField(map, id, 'wrap');
|
|
233
|
+
}
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
183
236
|
/**
|
|
184
237
|
* Server-built annotations added to a served result AFTER the untrusted marking (issue #102). Each
|
|
185
238
|
* `operations.fieldCatalog` resource row says which `autotask_query` operation serves it (or null:
|
|
@@ -295,6 +348,10 @@ export async function readOneItem(ctx, operation, args, signal, opts) {
|
|
|
295
348
|
}
|
|
296
349
|
const nameChecked = operation.endsWith('.query') || operation.endsWith('.count');
|
|
297
350
|
const result = await ctx.ops.invokeRead(ctx.client, operation, cleanArgs, opts, signal).catch((err) => {
|
|
351
|
+
// #224: a refused cursor the caller passed is the caller's input; a refused vendor continuation is not
|
|
352
|
+
if (CURSOR_OPS.has(operation) && err?.code === 'RESPONSE_REJECTED' && cleanArgs.some((a) => a !== null && typeof a === 'object' && typeof a.cursor === 'string')) {
|
|
353
|
+
Object.assign(err, { callerInput: true });
|
|
354
|
+
}
|
|
298
355
|
throw nameChecked ? rewriteFieldNameRefusal(err, operation, operation.slice(0, operation.lastIndexOf('.')), 'read') : err;
|
|
299
356
|
});
|
|
300
357
|
if (result === undefined)
|
|
@@ -312,6 +369,11 @@ export async function readOneItem(ctx, operation, args, signal, opts) {
|
|
|
312
369
|
const projected = result !== null && typeof result === 'object' ? markResult(result, crossResourceUntrusted()) : result;
|
|
313
370
|
if (isFileOperation(operation))
|
|
314
371
|
markFilePayload(projected);
|
|
372
|
+
// #224: the posted-state reads' nextCursor goes back verbatim as opts.cursor; the 500 cap would cut it
|
|
373
|
+
const cursor = CURSOR_OPS.has(operation) && projected !== null && typeof projected === 'object' ? projected.nextCursor : undefined;
|
|
374
|
+
// only a TimeEntries continuation URL is a cursor; anything else is vendor text (500 cap, wrapped)
|
|
375
|
+
if (typeof cursor === 'string')
|
|
376
|
+
markServerField(projected, 'nextCursor', isEntryCursor(cursor, ctx.origin) ? 'cursor' : 'wrap');
|
|
315
377
|
if (operation === 'operations.fieldCatalog')
|
|
316
378
|
annotateCatalogRows(projected);
|
|
317
379
|
return { result: projected, ...(note !== undefined ? { note } : {}), ...(scopeNote !== undefined ? { scopeNote } : {}) };
|
|
@@ -26,8 +26,8 @@ export declare const FIELD_INFO_EXPENSIVE = "Expensive: it returns the whole fie
|
|
|
26
26
|
* read paths (#183 S8, AC13): `autotask_query` (`via: 'query'`: its argument is include_fields, and the escape is
|
|
27
27
|
* an autotask_read call) and `autotask_read` of a `*.query` / `*.count` key (`via: 'read'`: the bag keeps
|
|
28
28
|
* includeFields and fieldCheck). The SDK message separates the causes, so the remedy follows the cause: a FILTER
|
|
29
|
-
* refusal names the UDF remedy (udf:true, max 1 per query, plus the include:["udf"], labels:true definitions read)
|
|
30
|
-
* and says the fieldCheck:false escape is for genuine vendor field names, never a UDF name; an includeFields
|
|
29
|
+
* refusal on a UDF-capable resource names the UDF remedy (udf:true, max 1 per query, plus the include:["udf"], labels:true definitions read)
|
|
30
|
+
* and says the fieldCheck:false escape is for genuine vendor field names, never a UDF name (any other resource: the vendor-name remedy only, no UDF text); an includeFields
|
|
31
31
|
* refusal carries no UDF advice (whether the vendor accepts a UDF name in IncludeFields is unverified) and keeps
|
|
32
32
|
* the #76 escape. Every suggestedAction stays within the 500-character cap for every query resource (tests).
|
|
33
33
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AutotaskConfigError } from 'node-autotask';
|
|
2
|
+
import { UDF_CAPABLE } from './catalog-data.js';
|
|
2
3
|
/**
|
|
3
4
|
* tools/field-pointer.ts — the one place the "how do I list this resource's field names" call is
|
|
4
5
|
* written (issues #102, #98). The bare `operations.fieldCatalog` serves the five workflow resources
|
|
@@ -33,8 +34,8 @@ export const FIELD_INFO_EXPENSIVE = 'Expensive: it returns the whole field table
|
|
|
33
34
|
* read paths (#183 S8, AC13): `autotask_query` (`via: 'query'`: its argument is include_fields, and the escape is
|
|
34
35
|
* an autotask_read call) and `autotask_read` of a `*.query` / `*.count` key (`via: 'read'`: the bag keeps
|
|
35
36
|
* includeFields and fieldCheck). The SDK message separates the causes, so the remedy follows the cause: a FILTER
|
|
36
|
-
* refusal names the UDF remedy (udf:true, max 1 per query, plus the include:["udf"], labels:true definitions read)
|
|
37
|
-
* and says the fieldCheck:false escape is for genuine vendor field names, never a UDF name; an includeFields
|
|
37
|
+
* refusal on a UDF-capable resource names the UDF remedy (udf:true, max 1 per query, plus the include:["udf"], labels:true definitions read)
|
|
38
|
+
* and says the fieldCheck:false escape is for genuine vendor field names, never a UDF name (any other resource: the vendor-name remedy only, no UDF text); an includeFields
|
|
38
39
|
* refusal carries no UDF advice (whether the vendor accepts a UDF name in IncludeFields is unverified) and keeps
|
|
39
40
|
* the #76 escape. Every suggestedAction stays within the 500-character cap for every query resource (tests).
|
|
40
41
|
*/
|
|
@@ -42,9 +43,12 @@ export function rewriteFieldNameRefusal(err, operation, resource, via) {
|
|
|
42
43
|
if (!(err instanceof AutotaskConfigError) || !err.message.includes('unknown field name'))
|
|
43
44
|
return err;
|
|
44
45
|
const escape = via === 'query' ? `autotask_read {operation:"${operation}", args:[{..., fieldCheck:false}]}` : 'fieldCheck:false in this args bag';
|
|
45
|
-
|
|
46
|
+
// a resource outside UDF_CAPABLE has no UDFs (describe says 'not udf-capable'): its filter refusal serves only the vendor-name remedy
|
|
47
|
+
const suggestedAction = err.message.includes('filter condition(s) carry unknown field name') && UDF_CAPABLE.has(resource)
|
|
46
48
|
? `Use exact vendor field names: ${scopedCatalogCall(resource)}. A user-defined (tenant) field: filter on it with udf:true (max 1 per query); its names and picklist ids: ${udfCatalogCall(resource)}. ${escape} is for a genuine vendor name only, never a UDF name.`
|
|
47
|
-
:
|
|
49
|
+
: err.message.includes('filter condition(s) carry unknown field name')
|
|
50
|
+
? `Use exact vendor field names: ${scopedCatalogCall(resource)}. To skip the name check for a genuine vendor name use ${escape}.`
|
|
51
|
+
: `Use exact vendor field names: ${scopedCatalogCall(resource)}. To skip the name check use ${escape}.`;
|
|
48
52
|
const stripped = err.message.replace(/\s*\(src\/field-sets\.ts\)/g, '');
|
|
49
53
|
return new AutotaskConfigError(via === 'query' ? stripped.replace(/includeFields/g, 'include_fields') : stripped, { operation, suggestedAction });
|
|
50
54
|
}
|
package/dist/tools/meta.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export declare function buildCatalog(ctx: ToolContext): BuiltTool;
|
|
|
10
10
|
export declare function paramNames(operation: string, rec: unknown): Record<string, unknown> | undefined;
|
|
11
11
|
/** `autotask_describe`: one operation's record + verdict + playbooks, or one resource's relationship map. */
|
|
12
12
|
export declare function buildDescribe(ctx: ToolContext): BuiltTool;
|
|
13
|
+
/** The single place to extend: entities the registry has no create for, yet the vendor documentation lists one, so the
|
|
14
|
+
* note would be false. `time_off_requests`: the swagger has a child create (POST /V1.0/Resources/{parentId}/TimeOffRequests,
|
|
15
|
+
* TimeOffRequestsChild_CreateEntity) and the SDK defines TimeOffRequestCreate; the SDK plan simply has no create row. */
|
|
16
|
+
export declare const NO_CREATE_EXCLUDED: ReadonlySet<string>;
|
|
17
|
+
/** The entities that carry the noCreate note (pinned by name in a test). */
|
|
18
|
+
export declare const NO_CREATE_ENTITIES: ReadonlySet<string>;
|
|
13
19
|
/** `autotask_read`: one read by exact key, or up to 10 (`items`) with per-item results. */
|
|
14
20
|
export declare function buildRead(ctx: ToolContext): BuiltTool;
|
|
15
21
|
/** `autotask_tenant_status`: identity / version / thresholds / server / time; each part degrades to typed absent. */
|
package/dist/tools/meta.js
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import { AutotaskConfigError } from 'node-autotask';
|
|
19
19
|
import { effectiveRequiredFields } from 'node-autotask/operations';
|
|
20
20
|
import { UDF_MERGE_NOTE, UDF_NONE, UDF_ROWS_NOTE, associationFields, associationRules, caveatNotes, constraintNotes, coupledNotes, dateConventionNotes, statusScopeNote, udfDefinitionRows, udfMergeNote, udfMoreNote, udfUnavailable } from './write-guidance.js';
|
|
21
|
+
import { scrubDeep, scrubProvenance } from './provenance-scrub.js';
|
|
21
22
|
import { CATALOG, RECORDS, SDK_VERSION, TOOL_NAME_BY_OP, UDF_CAPABLE, opsForTool, recordById, servedToolName, untrustedFieldsOf } from './catalog-data.js';
|
|
22
23
|
import { releaseClientOnError } from './context.js';
|
|
23
24
|
import { abortedAnswer, InputInvalidError, readOneItem, requestSecrets } from './dispatch.js';
|
|
@@ -252,12 +253,14 @@ const ENTITY_RESOURCES = new Set(RECORDS.filter((r) => r.id.endsWith('.getEntity
|
|
|
252
253
|
/** The single place to extend: entities the registry has no create for, yet the vendor documentation lists one, so the
|
|
253
254
|
* note would be false. `time_off_requests`: the swagger has a child create (POST /V1.0/Resources/{parentId}/TimeOffRequests,
|
|
254
255
|
* TimeOffRequestsChild_CreateEntity) and the SDK defines TimeOffRequestCreate; the SDK plan simply has no create row. */
|
|
255
|
-
const NO_CREATE_EXCLUDED = new Set(['time_off_requests']);
|
|
256
|
+
export const NO_CREATE_EXCLUDED = new Set(['time_off_requests']);
|
|
257
|
+
/** The entities that carry the noCreate note (pinned by name in a test). */
|
|
258
|
+
export const NO_CREATE_ENTITIES = new Set([...ENTITY_RESOURCES].filter((r) => !CREATE_RESOURCES.has(r) && !NO_CREATE_EXCLUDED.has(r)));
|
|
256
259
|
const NO_CREATE_NOTE = 'do not look for a create operation: this server has none for this entity, and the vendor API documentation lists none.';
|
|
257
260
|
/**
|
|
258
261
|
* S8 (#183): the UDF block for `describe {resource}`: the mechanism facts (SDK row shape) + the definitions
|
|
259
262
|
* pointer. Zero wire: the capability fact is the registry itself (UDF_CAPABLE: the resources whose records
|
|
260
|
-
*
|
|
263
|
+
* carry the published udfCapable flag, SDK 0.37.0 #254; SDK 0.34.0 #219 N7), and the definitions are the live tenant
|
|
261
264
|
* read the pointer names (the scoped fieldCatalog read with the `udf` include).
|
|
262
265
|
*/
|
|
263
266
|
function udfBlock(resource) {
|
|
@@ -314,7 +317,7 @@ async function guidanceFor(ctx, rec, reachable) {
|
|
|
314
317
|
// #173: the positional [from, to, opts] mixes types; the same window as one uniform-string filter
|
|
315
318
|
return {
|
|
316
319
|
exampleArgs: ['2026-10-20', '2026-10-22', { resourceID: 123 }],
|
|
317
|
-
sameWindowAsQuery: 'autotask_query {resource:"time_entries", filter:[{field:"resourceID",op:"eq",value:"123"},{field:"dateWorked",op:"gte",value:"2026-10-20"},{field:"dateWorked",op:"lte",value:"2026-10-22"}], limit:500}: the same rows up to limit (
|
|
320
|
+
sameWindowAsQuery: 'autotask_query {resource:"time_entries", filter:[{field:"resourceID",op:"eq",value:"123"},{field:"dateWorked",op:"gte",value:"2026-10-20"},{field:"dateWorked",op:"lte",value:"2026-10-22"}], limit:500}: the same rows up to limit (one window call returns at most 500 and a nextCursor to pass as cursor for the rest; dateWorked is a date, both days inclusive), without the posted counts, for a client that cannot send a mixed-type array.',
|
|
318
321
|
};
|
|
319
322
|
}
|
|
320
323
|
if (op === 'moveToTicket') {
|
|
@@ -350,8 +353,9 @@ async function guidanceFor(ctx, rec, reachable) {
|
|
|
350
353
|
// The SDK's one precedence (live-proven > zone > spec) is a create fact; an update keeps the vendor's own set.
|
|
351
354
|
const zone = op === 'create' ? await zoneFactsOf(ctx, rec.resource) : undefined;
|
|
352
355
|
facts = zone?.createGuidance ?? undefined;
|
|
353
|
-
//
|
|
354
|
-
|
|
356
|
+
// quirk statements are SDK free text: none is served on a create, only a pointer, and only when a quirk
|
|
357
|
+
// names the create op (SDK 0.37.0 #253 structured quirks; #224)
|
|
358
|
+
hasQuirks = (zone?.quirks ?? []).some((q) => Array.isArray(q?.ops) && q.ops.includes('create'));
|
|
355
359
|
const eff = op === 'create' ? effectiveRequiredFields({ requiredFields: spec }, zone) : { fields: spec, from: 'spec', unproven: true };
|
|
356
360
|
// eff.fields is already the unconditional set (value-gated fields are in eff.conditional, SDK 0.34.0)
|
|
357
361
|
required = eff.fields;
|
|
@@ -414,21 +418,32 @@ async function zoneFactsOf(ctx, resource) {
|
|
|
414
418
|
}
|
|
415
419
|
/** The one read envelope of autotask_read and autotask_fetch_many (#97), served on the on-demand surface. */
|
|
416
420
|
const READ_ENVELOPE = 'data = { results: Slot[], ok: <count>, failed: <count>, budget?, budgetWarning? }, always an array in input order, one slot for a single read. Slot = { ok: true, operation, result, windowNote?, scopeNote? } or { ok: false, operation, error }; autotask_fetch_many slots also echo resource and id (operation is "<resource>.get"; a miss is { ok: false, error: { code: "NOT_FOUND" } }). A single read that fails still answers the whole-call error envelope.';
|
|
417
|
-
/** Served relationship map
|
|
421
|
+
/** Served relationship map without the SDK's free text: createGuidance loses `vendorEvidence` and
|
|
422
|
+
* `blocker.message`/`remedy`; each structured `zoneFacts.quirks` entry loses `evidence` (SDK 0.37.0 #253: raw
|
|
423
|
+
* campaign strings) in {@link scrubDeep}, which drops every string `evidence` key and scrubs the rest (#224). */
|
|
418
424
|
function projectMap(map) {
|
|
419
425
|
const m = map;
|
|
420
|
-
const
|
|
421
|
-
if (
|
|
422
|
-
return map;
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
const z = m?.zoneFacts;
|
|
427
|
+
if (z === undefined || z === null || typeof z !== 'object')
|
|
428
|
+
return scrubDeep(map);
|
|
429
|
+
const zone = { ...z };
|
|
430
|
+
// #224: a live write keeps its op and outcome; its date and note are maintainer history
|
|
431
|
+
const lw = z.liveWrites;
|
|
432
|
+
if (Array.isArray(lw))
|
|
433
|
+
zone.liveWrites = lw.map((w) => (w !== null && typeof w === 'object' ? { op: w.op, outcome: w.outcome } : w));
|
|
434
|
+
const g = z.createGuidance;
|
|
435
|
+
if (g !== undefined && g !== null && typeof g === 'object') {
|
|
436
|
+
const slim = { ...g };
|
|
437
|
+
delete slim.vendorEvidence;
|
|
438
|
+
if (slim.blocker !== null && typeof slim.blocker === 'object') {
|
|
439
|
+
const b = { ...slim.blocker };
|
|
440
|
+
delete b.message;
|
|
441
|
+
delete b.remedy;
|
|
442
|
+
slim.blocker = b;
|
|
443
|
+
}
|
|
444
|
+
zone.createGuidance = slim;
|
|
430
445
|
}
|
|
431
|
-
return { ...m, zoneFacts:
|
|
446
|
+
return scrubDeep({ ...m, zoneFacts: zone });
|
|
432
447
|
}
|
|
433
448
|
/** Served record: `metadata.caveats` rows keep kind/fields/remedyOp, not the SDK's free-text `note`. */
|
|
434
449
|
function projectCaveats(rec) {
|
|
@@ -458,14 +473,15 @@ function servedUsage(rec) {
|
|
|
458
473
|
// the read-back is one `<resource>.get`: a create with no get in the registry answers `data.record: null`
|
|
459
474
|
return u.includes(CREATE_USAGE_FALSE) && recordById(`${rec.resource}.get`) !== undefined ? u.replace(CREATE_USAGE_FALSE, CREATE_USAGE_TRUE) : u;
|
|
460
475
|
}
|
|
461
|
-
/** Served record: caveats projected
|
|
476
|
+
/** Served record: caveats projected, the create usage corrected (AC24), the SDK's own test list dropped and
|
|
477
|
+
* its maintainer provenance scrubbed (#224). */
|
|
462
478
|
function servedRecord(rec) {
|
|
463
|
-
const projected = projectCaveats(rec);
|
|
479
|
+
const projected = { ...projectCaveats(rec) };
|
|
480
|
+
delete projected.tests;
|
|
464
481
|
const md = projected.metadata;
|
|
465
|
-
const usage = servedUsage(
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
return { ...projected, metadata: { ...md, usage } };
|
|
482
|
+
const usage = servedUsage(rec);
|
|
483
|
+
const out = typeof md?.usage !== 'string' || usage === undefined || md.usage === usage ? projected : { ...projected, metadata: { ...md, usage } };
|
|
484
|
+
return scrubDeep(out);
|
|
469
485
|
}
|
|
470
486
|
/** The per-field caps of a describe summary (characters): purpose, args and usage only, about 2 KB in all (#197). */
|
|
471
487
|
const SUMMARY_CAPS = { purpose: 500, args: 600, usage: 400 };
|
|
@@ -484,13 +500,13 @@ function summariseRecord(ctx, operation, viaTool) {
|
|
|
484
500
|
const reason = refusalReason(operation, rec, ctx.config, ctx.writable);
|
|
485
501
|
const md = (rec.metadata ?? {});
|
|
486
502
|
const row = CATALOG.catalog.find((r) => r.operation === operation);
|
|
487
|
-
const text = (v) => (typeof v === 'string' ? v : '');
|
|
503
|
+
const text = (v) => (typeof v === 'string' ? scrubProvenance(v) : '');
|
|
488
504
|
const cut = [];
|
|
489
505
|
const tool = viaTool ?? TOOL_NAME_BY_OP[operation];
|
|
490
506
|
return ok(`Summary of "${rec.id}" (purpose, args, usage).`, {
|
|
491
507
|
operation: rec.id,
|
|
492
508
|
...(tool !== undefined ? { tool, ...toolServed(ctx, tool) } : {}),
|
|
493
|
-
purpose: capText(text(md.purpose) || (row?.purpose
|
|
509
|
+
purpose: capText(text(md.purpose) || text(row?.purpose), SUMMARY_CAPS.purpose, 'purpose', cut),
|
|
494
510
|
requiredArgs: row?.requiredArgs ?? [],
|
|
495
511
|
args: capText(text(md.args), SUMMARY_CAPS.args, 'args', cut),
|
|
496
512
|
usage: capText(text(servedUsage(rec)), SUMMARY_CAPS.usage, 'usage', cut),
|
|
@@ -513,7 +529,7 @@ async function describeRecord(ctx, operation, viaTool, summary = false) {
|
|
|
513
529
|
...(guidance !== undefined ? { guidance } : {}),
|
|
514
530
|
...(rec.effect === 'read' && (viaTool === 'autotask_fetch_many' || (viaTool === undefined && !rec.id.endsWith('.query'))) ? { readEnvelope: READ_ENVELOPE } : {}),
|
|
515
531
|
record: { ...servedRecord(rec), untrustedFields: untrustedFieldsOf(rec) },
|
|
516
|
-
...(rec.effect === 'read' &&
|
|
532
|
+
...(rec.effect === 'read' && NO_CREATE_ENTITIES.has(rec.resource) ? { noCreate: NO_CREATE_NOTE } : {}),
|
|
517
533
|
reachable: reason === null,
|
|
518
534
|
why_not: reason,
|
|
519
535
|
playbooks: playbooksFor(rec.id),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/provenance-scrub.ts — maintainer provenance out of the SDK text `autotask_describe` serves (#224).
|
|
3
|
+
*
|
|
4
|
+
* The SDK's registry records and relationship maps carry its own build history inside otherwise useful
|
|
5
|
+
* guidance: the test zone (`ww29`), verification dates, issue numbers, campaign stages and markers, wall
|
|
6
|
+
* ids. None of it helps a model call the API, and some of it is tenant-identifying. Only the provenance
|
|
7
|
+
* tokens are removed: a parenthetical is split into clauses (`;`, ` — `, `, ` at its own depth) and only a
|
|
8
|
+
* clause that itself carries a marker is edited; a bare date survives unless its clause carries a marker
|
|
9
|
+
* (a format example such as `e.g. 2026-01-01` stays); a parenthetical left empty is dropped.
|
|
10
|
+
* Deterministic text surgery, not a guarantee: a marker the patterns below do not name passes through.
|
|
11
|
+
* ponytail: pattern list, extend it when an SDK bump adds a new marker shape.
|
|
12
|
+
*/
|
|
13
|
+
/** Object nesting bound for {@link scrubDeep}; deeper subtrees are dropped, never served unscrubbed. */
|
|
14
|
+
export declare const MAX_DEEP = 32;
|
|
15
|
+
export declare const MAX_PAREN_DEPTH = 16;
|
|
16
|
+
/** `text` with the provenance tokens removed (a text with no marker is returned as it is). */
|
|
17
|
+
export declare function scrubProvenance(text: string): string;
|
|
18
|
+
export declare function scrubExample(text: string): string;
|
|
19
|
+
/** A deep copy with every string scrubbed (`examples` by {@link scrubExample}) and the provenance-only string keys dropped. */
|
|
20
|
+
export declare function scrubDeep(value: unknown, depth?: number): unknown;
|