agent-cfml-check 0.1.1 → 0.2.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 +28 -4
- package/SPEC.md +19 -8
- package/dist/cli/index.js +15 -8
- package/dist/core/checker.d.ts +1 -0
- package/dist/core/checker.js +7 -10
- package/dist/core/lexer.js +25 -2
- package/dist/core/limits.js +4 -0
- package/dist/core/source-reader.js +50 -10
- package/dist/schema/types.d.ts +1 -1
- package/dist/schema/types.js +1 -1
- package/package.json +16 -2
- package/schema/agent-cfml-check-result-v1.schema.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Current status
|
|
6
6
|
|
|
7
|
-
Version `0.
|
|
7
|
+
Version `0.2.0` extends the `0.1.1` structural profile with parameter and exception tags. The implementation checks:
|
|
8
8
|
|
|
9
9
|
- paired CFML tag nesting;
|
|
10
10
|
- `cfelse` and `cfelseif` ownership and ordering;
|
|
@@ -17,7 +17,14 @@ Version `0.1.1` contains the first feasibility slice plus executable schema vali
|
|
|
17
17
|
- explicit-root path safety, source stability checks, input validation, output limits, nesting limits, finding limits, and time limits;
|
|
18
18
|
- deterministic JSON and text CLI output.
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Current repository source also recognizes `cfparam` as a bodyless tag and checks paired `cftry`, `cfcatch`, and `cffinally` structure. These changes are not included in the published `0.1.1` artifact or existing installed snapshots. They do not validate tag attributes, exception behavior, or runtime semantics. Self-closing paired tags, including `cfcatch`, return incomplete rather than an engine-validity claim.
|
|
21
|
+
|
|
22
|
+
The current local source corrects CFScript identifiers containing `cf` being
|
|
23
|
+
misclassified as markup. Ordinary variables and calls remain script text; actual
|
|
24
|
+
CFML tag islands still return `incomplete` with their source position. This fix
|
|
25
|
+
is included in the `0.2.0` source.
|
|
26
|
+
|
|
27
|
+
Historical verification for the `0.1.1` release baseline:
|
|
21
28
|
|
|
22
29
|
- `npm test`: 22/22 passed;
|
|
23
30
|
- `npm run typecheck`: passed;
|
|
@@ -27,10 +34,22 @@ Local verification before this documentation update:
|
|
|
27
34
|
- targeted Lucee `6.2.2.91` execution probe: passed;
|
|
28
35
|
- Ubuntu WSL Node `18.19.1` `npm ci`, tests, and typecheck: passed.
|
|
29
36
|
|
|
30
|
-
|
|
37
|
+
This is not a complete CFML parser or evidence of Adobe ColdFusion compatibility. A targeted Lucee `6.2.2.91` probe and an Ubuntu WSL Node `18.19.1` run for prior source do not establish a full engine or platform matrix for `0.2.0`.
|
|
31
38
|
|
|
32
39
|
## Install and use
|
|
33
40
|
|
|
41
|
+
Install the published CLI globally for Codex CLI or any terminal session:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npm install --global agent-cfml-check@latest
|
|
45
|
+
agent-cfml-check capabilities --json
|
|
46
|
+
agent-cfml-check check --root /path/to/repository path/to/file.cfm --json
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
The global installation exposes the `agent-cfml-check` command from any working
|
|
50
|
+
directory. For a checkout, use `npm ci`, `npm run build`, and the same command
|
|
51
|
+
through `node dist/cli/index.js`.
|
|
52
|
+
|
|
34
53
|
```sh
|
|
35
54
|
npm install
|
|
36
55
|
npm test
|
|
@@ -52,6 +71,11 @@ agent-cfml-check check --root <directory> <file> [--json] [--pretty]
|
|
|
52
71
|
|
|
53
72
|
Check limits can be overridden with `--max-source-bytes`, `--max-nesting`, `--max-findings`, `--max-output-bytes`, and `--time-limit-ms`, subject to the hard caps reported by `capabilities`.
|
|
54
73
|
|
|
74
|
+
`max_output_bytes` must be at least 512, reserving a complete failure envelope.
|
|
75
|
+
CLI ceilings count actual UTF-8 stdout, including indentation and the final
|
|
76
|
+
newline. If pretty output cannot fit, checking returns incomplete/exit 3 and may
|
|
77
|
+
emit its failure envelope as compact JSON. Default fitting output is unchanged.
|
|
78
|
+
|
|
55
79
|
The checker requires one explicit root and one `.cfm` or `.cfc` file. JSON mode emits one envelope on stdout; diagnostics stay on stderr. A completed check with structural violations still exits `0` and reports `data.verdict: "violations"`. Exit `3` means unsupported syntax, insufficient evidence, or a resource limit. Exit `4` means an explicit-root or access-policy rejection. See [SPEC.md](SPEC.md) for the complete contract.
|
|
56
80
|
|
|
57
81
|
## Safety and boundaries
|
|
@@ -76,4 +100,4 @@ The supported profile is `cfml-structure-v1`. Unknown/custom/imported tags, opti
|
|
|
76
100
|
- `dist/`: generated package output;
|
|
77
101
|
- `DESIGN.md`, `EPIC.md`, `ROADMAP.md`, `TASK.md`, `GOAL.md`, `PROGRESS.md`, `GOAL_PROMPT.md`, `CHANGELOG.md`: repository design, planning, status, and maintenance documents.
|
|
78
102
|
|
|
79
|
-
`package.json`
|
|
103
|
+
`package.json` includes `dist`, `README.md`, `SPEC.md`, `schema`, and `fixtures` in the package; the design, planning, progress, goal, prompt, changelog, and Agent Skill documents remain repository-maintainer documentation. The repository exposes the TypeScript library through `main`, `types`, and `exports`. The source package version is `0.2.0`.
|
package/SPEC.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Agent CFML Check Specification
|
|
2
2
|
|
|
3
3
|
**Status:** frozen feasibility contract for implementation baseline `c76b8be`, extended by release hardening
|
|
4
|
-
**Tool version:** `0.
|
|
4
|
+
**Tool version:** `0.2.0`
|
|
5
5
|
**Profile:** `cfml-structure-v1`
|
|
6
6
|
**Envelope schema:** `schema/agent-cfml-check-result-v1.schema.json`
|
|
7
7
|
|
|
@@ -22,11 +22,11 @@ agent-cfml-check check --root <directory> <file> [--json] [--pretty]
|
|
|
22
22
|
|
|
23
23
|
`capabilities` reports the profile, operations, extensions, tag catalogue, effective default limits, and exclusions. The CLI does not accept a file, root, or check-limit override for this operation.
|
|
24
24
|
|
|
25
|
-
`check` requires exactly one explicit root and one `.cfm` or `.cfc` file. `--pretty` requires `--json`. `--help` prints usage and `--version` prints `0.
|
|
25
|
+
`check` requires exactly one explicit root and one `.cfm` or `.cfc` file. `--pretty` requires `--json`. `--help` prints usage and `--version` prints `0.2.0`; neither may be combined with other options.
|
|
26
26
|
|
|
27
27
|
### TypeScript interface
|
|
28
28
|
|
|
29
|
-
`src/index.ts` exports `capabilities`, `checkFile`, `DEFAULT_LIMITS`, `HARD_LIMITS`, `normalizeLimits`, and schema types. Repository package metadata declares `main: "./dist/index.js"`, `types: "./dist/index.d.ts"`, and an import/types `exports` map. Package-surface tests and a temporary packed-install CLI/library smoke check
|
|
29
|
+
`src/index.ts` exports `capabilities`, `checkFile`, `DEFAULT_LIMITS`, `HARD_LIMITS`, `normalizeLimits`, and schema types. Repository package metadata declares `main: "./dist/index.js"`, `types: "./dist/index.d.ts"`, and an import/types `exports` map. Package-surface tests and a temporary packed-install CLI/library smoke check verified the previously published `0.1.1` artifact.
|
|
30
30
|
|
|
31
31
|
The TypeScript `capabilities(limitsInput)` API may receive limit overrides, while the CLI `capabilities` operation rejects them.
|
|
32
32
|
|
|
@@ -34,15 +34,17 @@ The TypeScript `capabilities(limitsInput)` API may receive limit overrides, whil
|
|
|
34
34
|
|
|
35
35
|
### Paired bodies
|
|
36
36
|
|
|
37
|
-
`cfcomponent`, `cffunction`, `cfif`, `cfloop`, `cfoutput`, `cfquery`, `cfsavecontent`, `cfscript`, and `
|
|
37
|
+
`cfcatch`, `cfcomponent`, `cffinally`, `cffunction`, `cfif`, `cfloop`, `cfoutput`, `cfquery`, `cfsavecontent`, `cfscript`, `cfsilent`, and `cftry` require matching closing tags in this profile. Self-closing spellings of paired tags return incomplete; this does not assert that every CFML engine rejects them.
|
|
38
38
|
|
|
39
39
|
### Branches
|
|
40
40
|
|
|
41
41
|
`cfelse` and `cfelseif` must belong to the directly open `cfif`. Only one `cfelse` is accepted, and `cfelseif` cannot follow `cfelse`.
|
|
42
42
|
|
|
43
|
+
`cfcatch` and `cffinally` must belong to the directly open `cftry`. A closed `cftry` must contain at least one direct `cfcatch` or `cffinally`; multiple catches are allowed, at most one `cffinally` is allowed, and catches cannot follow it. Nested `cftry` blocks have independent handlers. The scanner does not validate catch attributes, exception types, or arbitrary text and statements between handlers. Support for these tags is a source change after the published `0.1.1` artifact.
|
|
44
|
+
|
|
43
45
|
### Bodyless tags
|
|
44
46
|
|
|
45
|
-
`cfargument`, `cfinclude`, `cfqueryparam`, `cfreturn`, and `cfset` are standalone. Ordinary and self-closing spellings are accepted. Closing a bodyless tag is unsupported.
|
|
47
|
+
`cfargument`, `cfinclude`, `cfparam`, `cfqueryparam`, `cfreturn`, and `cfset` are standalone. Ordinary and self-closing spellings are accepted. Closing a bodyless tag is unsupported. `cfparam` support is a source change after the published `0.1.1` artifact; it does not validate attributes or runtime semantics.
|
|
46
48
|
|
|
47
49
|
Tag names are case-insensitive. Unknown, custom, imported, and unlisted tags are unsupported.
|
|
48
50
|
|
|
@@ -91,9 +93,18 @@ Check data contains the profile, source path/SHA-256/byte size/UTF-8/BOM metadat
|
|
|
91
93
|
|
|
92
94
|
Requested limits must be positive safe integers and cannot exceed hard caps. Reaching a limit returns a fail-closed result rather than partial data.
|
|
93
95
|
|
|
96
|
+
`max_output_bytes` has a minimum of 512 to reserve the compact failure envelope
|
|
97
|
+
at every hard-limit combination. CLI accounting includes the selected rendering
|
|
98
|
+
and final newline. An oversized result is withheld as incomplete with exit 3;
|
|
99
|
+
pretty mode falls back to compact failure JSON if indentation cannot fit.
|
|
100
|
+
|
|
94
101
|
## 7. Input and safety behavior
|
|
95
102
|
|
|
96
|
-
The source reader requires `.cfm` or `.cfc`, an existing root and file, realpath containment, a regular file, valid UTF-8, no NUL byte, and an effective source-size limit.
|
|
103
|
+
The source reader requires `.cfm` or `.cfc`, an existing root and file, realpath containment, a regular file, valid UTF-8, no NUL byte, and an effective source-size limit. Static symlink escapes are rejected before admission.
|
|
104
|
+
|
|
105
|
+
Reading is bound to a regular-file descriptor opened with no-follow and nonblocking flags where supported. Before consuming bytes, the reader compares admitted and opened device/inode identity, mode, size and nanosecond modification/change timestamps, checks the current pathname state, and rechecks the canonical path. Observed leaf or ancestor substitutions return the existing `SOURCE_CHANGED`/incomplete/exit 3 result. The same descriptor and pathname state are checked after reading, and the descriptor is closed on success or rejection.
|
|
106
|
+
|
|
107
|
+
Oversized metadata is rejected before body reads. Actual reads are bounded to the admitted size plus one byte for growth detection and cannot exceed the selected source-byte limit plus one. Over-limit growth returns `LIMIT_EXCEEDED`/incomplete/exit 3 before UTF-8 decoding. These checks reject observed mutation; they do not provide an atomic filesystem snapshot or operating-system path confinement.
|
|
97
108
|
|
|
98
109
|
Known envelope error codes include `INVALID_ARGUMENT`, `INVALID_LIMIT`, `UNSUPPORTED_EXTENSION`, `ROOT_NOT_FOUND`, `FILE_NOT_FOUND`, `PATH_UNRESOLVED`, `FILE_OUTSIDE_ROOT`, `LIMIT_EXCEEDED`, `SOURCE_CHANGED`, `BINARY_INPUT`, `ENCODING_UNSUPPORTED`, `UNSUPPORTED_SYNTAX`, and `INTERNAL_ERROR`.
|
|
99
110
|
|
|
@@ -111,6 +122,6 @@ JSON mode emits one envelope on stdout; failure diagnostics are also written to
|
|
|
111
122
|
|
|
112
123
|
## 9. Evidence boundary
|
|
113
124
|
|
|
114
|
-
The profile does not validate full CFML grammar, expression/type/runtime semantics, HTML, SQL, include expansion, directory/project context, Lucee/Adobe execution, unknown/custom/imported tags, or CFML tag islands inside `cfscript`. The `
|
|
125
|
+
The profile does not validate full CFML grammar, expression/type/runtime semantics, HTML, SQL, include expansion, directory/project context, Lucee/Adobe execution, unknown/custom/imported tags, or CFML tag islands inside `cfscript`. The prior `0.1.1` artifact was published and registry-verified. A targeted Lucee `6.2.2.91` probe and Ubuntu WSL Node `18.19.1` verification for prior source do not establish a full engine matrix for `0.2.0`.
|
|
115
126
|
|
|
116
|
-
The repository tests cover CF-01 through CF-13, pure-script `.cfc` handling,
|
|
127
|
+
The repository tests cover CF-01 through CF-13, pure-script `.cfc` handling, CLI behavior, package exports, and the parameter and exception tag additions. Full Lucee/Adobe engine comparison and a broader platform matrix remain unverified.
|
package/dist/cli/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { capabilities, checkFile } from "../core/checker.js";
|
|
2
|
+
import { capabilities, checkFile, fitOutput } from "../core/checker.js";
|
|
3
3
|
import { DEFAULT_LIMITS } from "../core/limits.js";
|
|
4
4
|
import { ToolFailure } from "../schema/errors.js";
|
|
5
5
|
import { TOOL_ID, TOOL_VERSION } from "../schema/types.js";
|
|
@@ -128,14 +128,21 @@ export function main(argv = process.argv.slice(2)) {
|
|
|
128
128
|
const result = args.operation === "capabilities"
|
|
129
129
|
? { envelope: capabilities(args.limits), exit_code: 0 }
|
|
130
130
|
: checkFile({ root: args.root, file: args.file, limits: args.limits });
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
131
|
+
const serialize = (value) => args.json
|
|
132
|
+
? `${JSON.stringify(value, null, args.pretty ? 2 : undefined)}\n`
|
|
133
|
+
: renderText(value);
|
|
134
|
+
const envelope = args.operation === "check"
|
|
135
|
+
? fitOutput(result.envelope, result.envelope.meta.limits, serialize)
|
|
136
|
+
: result.envelope;
|
|
137
|
+
let output = serialize(envelope);
|
|
138
|
+
// A compact failure retains the complete envelope when indentation cannot fit.
|
|
139
|
+
if (args.json && Buffer.byteLength(output) > envelope.meta.limits.max_output_bytes)
|
|
140
|
+
output = `${JSON.stringify(envelope)}\n`;
|
|
141
|
+
process.stdout.write(output);
|
|
142
|
+
if (envelope.status !== "ok")
|
|
143
|
+
for (const error of envelope.errors)
|
|
137
144
|
process.stderr.write(`error: ${error.code}: ${error.message}\n`);
|
|
138
|
-
return result.exit_code;
|
|
145
|
+
return envelope.status === "incomplete" ? 3 : result.exit_code;
|
|
139
146
|
}
|
|
140
147
|
catch (error) {
|
|
141
148
|
const failure = error instanceof ToolFailure ? error : new ToolFailure("INTERNAL_ERROR", "The CLI failed without a safe result", 1);
|
package/dist/core/checker.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CapabilitiesData, CheckData, CheckLimits, Envelope, CheckOptions } from "../schema/types.js";
|
|
2
|
+
export declare function fitOutput<T>(envelope: Envelope<T>, limits: CheckLimits, serialize?: (value: Envelope<T>) => string): Envelope<T>;
|
|
2
3
|
export declare function capabilities(limitsInput?: Partial<CheckLimits>): Envelope<CapabilitiesData>;
|
|
3
4
|
export declare function checkFile(options: CheckOptions): {
|
|
4
5
|
envelope: Envelope<CheckData>;
|
package/dist/core/checker.js
CHANGED
|
@@ -11,14 +11,12 @@ function meta(limits) {
|
|
|
11
11
|
function emptyEnvelope(status, limits, errors) {
|
|
12
12
|
return { schema_version: "1.0.0", tool: { id: TOOL_ID, version: TOOL_VERSION }, status, complete: false, data: null, errors, warnings: [], meta: meta(limits) };
|
|
13
13
|
}
|
|
14
|
-
function fitOutput(envelope, limits) {
|
|
15
|
-
const serialized =
|
|
14
|
+
export function fitOutput(envelope, limits, serialize = (value) => `${JSON.stringify(value)}\n`) {
|
|
15
|
+
const serialized = serialize(envelope);
|
|
16
16
|
if (Buffer.byteLength(serialized, "utf8") <= limits.max_output_bytes)
|
|
17
17
|
return envelope;
|
|
18
18
|
const fallback = emptyEnvelope("incomplete", limits, [{ code: "LIMIT_EXCEEDED", message: "The output limit was reached; result data was withheld" }]);
|
|
19
|
-
|
|
20
|
-
return fallback;
|
|
21
|
-
return { schema_version: "1.0.0", tool: { id: TOOL_ID, version: TOOL_VERSION }, status: "incomplete", complete: false, data: null, errors: [{ code: "LIMIT_EXCEEDED", message: "Output limit exceeded" }], warnings: [], meta: meta(limits) };
|
|
19
|
+
return fallback;
|
|
22
20
|
}
|
|
23
21
|
export function capabilities(limitsInput) {
|
|
24
22
|
const limits = normalizeLimits(limitsInput);
|
|
@@ -82,13 +80,12 @@ export function checkFile(options) {
|
|
|
82
80
|
],
|
|
83
81
|
findings: scanned.findings,
|
|
84
82
|
};
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
exit_code: 0,
|
|
88
|
-
};
|
|
83
|
+
const envelope = fitOutput({ schema_version: "1.0.0", tool: { id: TOOL_ID, version: TOOL_VERSION }, status: "ok", complete: true, data, errors: [], warnings: [], meta: meta(limits) }, limits);
|
|
84
|
+
return { envelope, exit_code: envelope.status === "incomplete" ? 3 : 0 };
|
|
89
85
|
}
|
|
90
86
|
catch (error) {
|
|
91
87
|
const failure = error instanceof ToolFailure ? error : new ToolFailure("INTERNAL_ERROR", "The checker failed without a safe result", 1);
|
|
92
|
-
|
|
88
|
+
const envelope = fitOutput(emptyEnvelope(failure.exitCode === 3 ? "incomplete" : "error", limits, [{ code: failure.code, message: failure.message }]), limits);
|
|
89
|
+
return { envelope, exit_code: envelope.status === "incomplete" ? 3 : failure.exitCode };
|
|
93
90
|
}
|
|
94
91
|
}
|
package/dist/core/lexer.js
CHANGED
|
@@ -10,13 +10,18 @@ export const PAIRED_TAGS = new Set([
|
|
|
10
10
|
"cfcomponent",
|
|
11
11
|
"cffunction",
|
|
12
12
|
"cfscript",
|
|
13
|
+
"cftry",
|
|
14
|
+
"cfcatch",
|
|
15
|
+
"cffinally",
|
|
13
16
|
]);
|
|
14
17
|
export const BRANCH_TAGS = new Set(["cfelseif", "cfelse"]);
|
|
15
|
-
export const BODYLESS_TAGS = new Set(["cfset", "cfreturn", "cfinclude", "cfargument", "cfqueryparam"]);
|
|
18
|
+
export const BODYLESS_TAGS = new Set(["cfset", "cfreturn", "cfinclude", "cfargument", "cfqueryparam", "cfparam"]);
|
|
16
19
|
function isNameCharacter(character) {
|
|
17
20
|
return character !== undefined && /[A-Za-z0-9_:-]/u.test(character);
|
|
18
21
|
}
|
|
19
22
|
function startsCfTag(source, index) {
|
|
23
|
+
if (source[index] !== "<")
|
|
24
|
+
return false;
|
|
20
25
|
const afterSlash = source[index + 1] === "/" ? index + 2 : index + 1;
|
|
21
26
|
return source.slice(afterSlash, afterSlash + 2).toLowerCase() === "cf" && isNameCharacter(source[afterSlash + 2]);
|
|
22
27
|
}
|
|
@@ -291,7 +296,22 @@ function processTag(context, token, stack, findings) {
|
|
|
291
296
|
throw new ToolFailure("UNSUPPORTED_SYNTAX", `Paired tag "${token.name}" cannot be self-closing`, 3);
|
|
292
297
|
if (stack.length >= context.max_nesting)
|
|
293
298
|
throw new ToolFailure("LIMIT_EXCEEDED", "The tag nesting limit was reached; no partial success is returned", 3);
|
|
294
|
-
|
|
299
|
+
if (token.name === "cfcatch" || token.name === "cffinally") {
|
|
300
|
+
const current = stack[stack.length - 1];
|
|
301
|
+
if (current?.name !== "cftry") {
|
|
302
|
+
addFinding(context, findings, finding(context, "INVALID_BRANCH", `${token.name} must belong to the directly open cftry`, token.start, token.end));
|
|
303
|
+
}
|
|
304
|
+
else {
|
|
305
|
+
if (current.try_finally_seen) {
|
|
306
|
+
const message = token.name === "cffinally" ? "cftry cannot contain more than one cffinally" : "cfcatch cannot appear after cffinally";
|
|
307
|
+
addFinding(context, findings, finding(context, "INVALID_BRANCH", message, token.start, token.end, current));
|
|
308
|
+
}
|
|
309
|
+
current.try_handler_seen = true;
|
|
310
|
+
if (token.name === "cffinally")
|
|
311
|
+
current.try_finally_seen = true;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
stack.push({ name: token.name, start: token.start, end: token.end, branch_else_seen: false, try_handler_seen: false, try_finally_seen: false });
|
|
295
315
|
return;
|
|
296
316
|
}
|
|
297
317
|
if (token.name === "cfscript" && stack[stack.length - 1]?.name !== "cfscript") {
|
|
@@ -310,6 +330,9 @@ function processTag(context, token, stack, findings) {
|
|
|
310
330
|
addFinding(context, findings, finding(context, "MISMATCHED_CLOSE", `Closing tag "${token.name}" does not match open "${current.name}"`, token.start, token.end, current));
|
|
311
331
|
return;
|
|
312
332
|
}
|
|
333
|
+
if (current.name === "cftry" && !current.try_handler_seen) {
|
|
334
|
+
addFinding(context, findings, finding(context, "INVALID_BRANCH", "cftry must contain a directly nested cfcatch or cffinally", token.start, token.end, current));
|
|
335
|
+
}
|
|
313
336
|
stack.pop();
|
|
314
337
|
}
|
|
315
338
|
export function scanCfml(source, limits) {
|
package/dist/core/limits.js
CHANGED
|
@@ -28,5 +28,9 @@ export function normalizeLimits(requested) {
|
|
|
28
28
|
}
|
|
29
29
|
values[key] = value;
|
|
30
30
|
}
|
|
31
|
+
// Reserve enough space for the compact failure envelope at every hard limit.
|
|
32
|
+
if (values.max_output_bytes < 512) {
|
|
33
|
+
throw new ToolFailure("INVALID_LIMIT", "max_output_bytes must be at least 512", 2);
|
|
34
|
+
}
|
|
31
35
|
return values;
|
|
32
36
|
}
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { existsSync,
|
|
1
|
+
import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readSync, realpathSync, statSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { TextDecoder } from "node:util";
|
|
4
4
|
import { ToolFailure } from "../schema/errors.js";
|
|
5
5
|
function escapesRoot(root, candidate) {
|
|
6
6
|
const relative = path.relative(root, candidate);
|
|
7
|
-
return relative.startsWith(
|
|
7
|
+
return relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative);
|
|
8
|
+
}
|
|
9
|
+
function sameFileState(left, right) {
|
|
10
|
+
return left.dev === right.dev && left.ino === right.ino && left.mode === right.mode &&
|
|
11
|
+
left.size === right.size && left.mtimeNs === right.mtimeNs && left.ctimeNs === right.ctimeNs;
|
|
8
12
|
}
|
|
9
13
|
export function loadSource(options) {
|
|
10
14
|
if (!/\.(?:cfm|cfc)$/iu.test(options.file)) {
|
|
@@ -32,19 +36,55 @@ export function loadSource(options) {
|
|
|
32
36
|
if (escapesRoot(realRoot, resolvedPath)) {
|
|
33
37
|
throw new ToolFailure("FILE_OUTSIDE_ROOT", "The selected file resolves outside the explicit root", 4);
|
|
34
38
|
}
|
|
35
|
-
const before = statSync(resolvedPath);
|
|
39
|
+
const before = statSync(resolvedPath, { bigint: true });
|
|
36
40
|
if (!before.isFile())
|
|
37
41
|
throw new ToolFailure("FILE_NOT_FOUND", "The selected path is not a regular file", 2);
|
|
38
|
-
if (before.size > options.limits.max_source_bytes) {
|
|
42
|
+
if (before.size > BigInt(options.limits.max_source_bytes)) {
|
|
39
43
|
throw new ToolFailure("LIMIT_EXCEEDED", `Source exceeds the ${options.limits.max_source_bytes}-byte limit`, 3);
|
|
40
44
|
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
+
let descriptor;
|
|
46
|
+
let bytes;
|
|
47
|
+
const changed = () => new ToolFailure("SOURCE_CHANGED", "The admitted source changed before or during reading", 3);
|
|
48
|
+
const exceeded = () => new ToolFailure("LIMIT_EXCEEDED", "Source exceeds the configured byte limit", 3);
|
|
49
|
+
try {
|
|
50
|
+
// Admission must describe the descriptor we read, including ancestor replacements.
|
|
51
|
+
descriptor = openSync(resolvedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
52
|
+
const opened = fstatSync(descriptor, { bigint: true });
|
|
53
|
+
if (!opened.isFile())
|
|
54
|
+
throw changed();
|
|
55
|
+
if (opened.size > BigInt(options.limits.max_source_bytes))
|
|
56
|
+
throw exceeded();
|
|
57
|
+
const current = lstatSync(resolvedPath, { bigint: true });
|
|
58
|
+
if (!current.isFile() || !sameFileState(before, opened) || !sameFileState(opened, current) ||
|
|
59
|
+
realpathSync(resolvedPath) !== resolvedPath)
|
|
60
|
+
throw changed();
|
|
61
|
+
const buffer = Buffer.alloc(Number(opened.size) + 1);
|
|
62
|
+
let length = 0;
|
|
63
|
+
while (length < buffer.length) {
|
|
64
|
+
const count = readSync(descriptor, buffer, length, buffer.length - length, length);
|
|
65
|
+
if (count === 0)
|
|
66
|
+
break;
|
|
67
|
+
length += count;
|
|
68
|
+
if (length > options.limits.max_source_bytes)
|
|
69
|
+
throw exceeded();
|
|
70
|
+
}
|
|
71
|
+
const after = fstatSync(descriptor, { bigint: true });
|
|
72
|
+
if (after.size > BigInt(options.limits.max_source_bytes))
|
|
73
|
+
throw exceeded();
|
|
74
|
+
if (!sameFileState(opened, after) || after.size !== BigInt(length) ||
|
|
75
|
+
!sameFileState(after, lstatSync(resolvedPath, { bigint: true })) ||
|
|
76
|
+
realpathSync(resolvedPath) !== resolvedPath)
|
|
77
|
+
throw changed();
|
|
78
|
+
bytes = buffer.subarray(0, length);
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (error instanceof ToolFailure)
|
|
82
|
+
throw error;
|
|
83
|
+
throw changed();
|
|
45
84
|
}
|
|
46
|
-
|
|
47
|
-
|
|
85
|
+
finally {
|
|
86
|
+
if (descriptor !== undefined)
|
|
87
|
+
closeSync(descriptor);
|
|
48
88
|
}
|
|
49
89
|
if (bytes.includes(0))
|
|
50
90
|
throw new ToolFailure("BINARY_INPUT", "Binary or NUL-containing input is not supported", 2);
|
package/dist/schema/types.d.ts
CHANGED
package/dist/schema/types.js
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-cfml-check",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Deterministic, read-only structural checks for a bounded CFML subset.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/yapweijun1996/AI-Agent-Tools.git",
|
|
8
|
+
"directory": "packages/cfml-check"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/yapweijun1996/AI-Agent-Tools/tree/main/packages/cfml-check#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/yapweijun1996/AI-Agent-Tools/issues"
|
|
13
|
+
},
|
|
5
14
|
"type": "module",
|
|
6
15
|
"license": "MIT",
|
|
7
16
|
"main": "./dist/index.js",
|
|
@@ -28,9 +37,14 @@
|
|
|
28
37
|
"scripts": {
|
|
29
38
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
30
39
|
"build": "tsc -p tsconfig.build.json",
|
|
31
|
-
"test": "npm run build &&
|
|
40
|
+
"test": "npm run build && npm run test:prepared",
|
|
41
|
+
"test:prepared": "tsc -p tsconfig.test.json && node --test .test-dist/test/check.test.js .test-dist/test/cli.test.js .test-dist/test/package.test.js .test-dist/test/schema.test.js",
|
|
32
42
|
"prepack": "npm run build"
|
|
33
43
|
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"registry": "https://registry.npmjs.org/",
|
|
46
|
+
"access": "public"
|
|
47
|
+
},
|
|
34
48
|
"devDependencies": {
|
|
35
49
|
"@types/node": "^22.10.0",
|
|
36
50
|
"ajv": "^8.20.0",
|
|
@@ -118,7 +118,7 @@
|
|
|
118
118
|
"max_source_bytes": { "type": "integer", "minimum": 1 },
|
|
119
119
|
"max_nesting": { "type": "integer", "minimum": 1 },
|
|
120
120
|
"max_findings": { "type": "integer", "minimum": 1 },
|
|
121
|
-
"max_output_bytes": { "type": "integer", "minimum":
|
|
121
|
+
"max_output_bytes": { "type": "integer", "minimum": 512 },
|
|
122
122
|
"time_limit_ms": { "type": "integer", "minimum": 1 }
|
|
123
123
|
},
|
|
124
124
|
"additionalProperties": false
|