@blogic-cz/agent-tools 1.5.9 → 1.5.11
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 +6 -0
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/gh-tool/api.d.ts.map +1 -1
- package/dist/gh-tool/environment.d.ts +2 -0
- package/dist/gh-tool/environment.d.ts.map +1 -0
- package/dist/gh-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/text-input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/credential-guard/index.ts +22 -32
- package/src/gh-tool/api.ts +50 -24
- package/src/gh-tool/environment.ts +50 -0
- package/src/gh-tool/service.ts +3 -1
- package/src/gh-tool/text-input.ts +7 -1
- package/src/shared/bun.ts +1 -1
package/README.md
CHANGED
|
@@ -612,6 +612,10 @@ The guard blocks agents from accessing sensitive files, leaking secrets, and run
|
|
|
612
612
|
|
|
613
613
|
GitHub text and file publication, database queries, Azure and Kubernetes command arguments, and observability requests are checked before execution. GitHub file uploads use snapshots of the inspected bytes. Generated release notes and existing pending reviews or draft releases are inspected before publication. Authentication headers and connection credentials remain inside the tools.
|
|
614
614
|
|
|
615
|
+
GitHub API requests validate the serialized body that will be sent, including JSON keys and decoded string values. Serialization happens once, before authentication lookup or network access. File publication checks both the supplied path and its resolved target against known credential locations before reading content. This includes SSH, AWS, Kubernetes, Azure, and common CLI credential stores. An unknown filename can still contain credentials, so content checks also apply.
|
|
616
|
+
|
|
617
|
+
The two direct GitHub CLI launch paths receive an explicit environment containing supported GitHub authentication, host/configuration, proxy/CA, and platform variables. Unrelated parent variables and runtime loader settings are not inherited. Other tools retain their own environment contracts; this does not isolate the parent agent or prevent GitHub CLI access to its configured credential files.
|
|
618
|
+
|
|
615
619
|
Results retain their JSON or TOON structure, with detected credentials replaced by `[REDACTED]`. Audit records are sanitized before storage; reading older records also sanitizes the returned content but does not rewrite the database.
|
|
616
620
|
|
|
617
621
|
These checks apply to these wrappers. They do not intercept arbitrary shell programs or other agent tools. GitHub does not offer an atomic content-version condition for submitting pending reviews or publishing existing drafts, so a concurrent remote edit between inspection and publication remains possible.
|
|
@@ -654,6 +658,8 @@ Paths are checked against sensitive filename patterns and configured path rules.
|
|
|
654
658
|
|
|
655
659
|
Single literal `echo`/`printf` writes with output redirection and single `cat`/`tee` heredoc writes are allowed. Heredoc braces are data. Use a quoted delimiter for literal code; unquoted heredocs containing `$`, backticks, or backslashes do not receive the literal-write exception. The exception permits only supported combinations with navigation or selected inspections; unsupported following commands remain outside it. Supported complete Python stdin heredocs are checked as inline programs, with the normal runtime-argument, path, execution, and environment checks.
|
|
656
660
|
|
|
661
|
+
Python JSON parsing, dictionary iteration, and printing are not themselves evidence of credential access. They follow the same access and execution rules as other supported inline programs. This intentionally permits ordinary JSON output that older versions refused. Unknown secrets in ordinary files can therefore reach arbitrary shell output; wrapper content checks do not protect that output. A refusal caused by unsupported syntax means the guard could not establish its policy conditions, not that it observed a secret being read.
|
|
662
|
+
|
|
657
663
|
A separate bounded Node proof supports literal `package.json` inventory loops that print `bin` metadata or use the supported scripts filter. The complete invocation must preserve loader identity through known passive commands, closed literal writers, or proved inventories. Startup/environment mutations, unknown commands, loader changes, and other dynamic loading remain unproved and are refused.
|
|
658
664
|
|
|
659
665
|
### Setup for OpenCode
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credential-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAI5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM/D,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAAC;AAQF,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACxE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3E,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,sBAAsB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjF,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxD,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/credential-guard/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAmB,qBAAqB,EAAE,MAAM,eAAe,CAAC;AAI5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,6BAA6B,CAAC;AAChE,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAM/D,8CAA8C;AAC9C,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,+CAA+C;AAC/C,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B,CAAC;AAQF,+CAA+C;AAC/C,MAAM,MAAM,eAAe,GAAG;IAC5B,uBAAuB,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAC;IACxE,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3E,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAC7C,sBAAsB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACrD,iBAAiB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IACjF,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kBAAkB,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CACxD,CAAC;AAg7DF,6CAA6C;AAC7C,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAErE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAED,2CAA2C;AAC3C,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAEpE;AAMD;;;;;GAKG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,CAAC,EAAE,qBAAqB,GAAG,eAAe,CAiTrF;AAQD,kEAAkE;AAClE,eAAO,MAAM,uBAAuB,UA3wED,SAAS,UAAU,UAAU,KAAK,IA2wEM,CAAC;AAE5E,oDAAoD;AACpD,eAAO,MAAM,aAAa,YA7wEC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG,IA6wEjB,CAAC;AAExD,yEAAyE;AACzE,eAAO,MAAM,aAAa,aA/wEE,MAAM,KAAK,OA+wEgB,CAAC;AAExD,yDAAyD;AACzD,eAAO,MAAM,aAAa,aAjxEE,MAAM,KAAK,OAixEgB,CAAC;AAExD,oEAAoE;AACpE,eAAO,MAAM,sBAAsB,YAnxEC,MAAM,KAAK,OAmxE0B,CAAC;AAE1E,iDAAiD;AACjD,eAAO,MAAM,iBAAiB,YArxEC,MAAM,KAAK;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAqxEf,CAAC;AAEhE,wDAAwD;AACxD,eAAO,MAAM,kBAAkB,YAvxEC,MAAM,KAAK,OAuxEsB,CAAC;AAElE,8EAA8E;AAC9E,eAAO,MAAM,kBAAkB,YAzxEC,MAAM,KAAK,MAAM,GAAG,IAyxEa,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/gh-tool/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,OAAO,EAAE,eAAe,EAA2C,MAAM,UAAU,CAAC;AACpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/gh-tool/api.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,OAAO,EAAE,eAAe,EAA2C,MAAM,UAAU,CAAC;AACpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAW/C,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,CAAC,CAAC;CACT,CAAC;AAYF,eAAO,MAAM,kBAAkB,qDA6B7B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,GAAG,QAAQ,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;CAC7B,CAAC;AAuFF,eAAO,MAAM,SAAS,GAAI,CAAC,QACnB,gBAAgB,KACrB,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,cAAc,CAqCjD,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/gh-tool/environment.ts"],"names":[],"mappings":"AAyCA,eAAO,MAAM,aAAa,iBACX,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,KAC9C,MAAM,CAAC,MAAM,EAAE,MAAM,CAMrB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/gh-tool/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AAExD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"service.d.ts","sourceRoot":"","sources":["../../src/gh-tool/service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC5E,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAU,MAAM,QAAQ,CAAC;AAExD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAChD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAIxC,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,UAAU,CAAC;AAGpF,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,OAAO,CAAC;AACjE,OAAO,EAAE,aAAa,EAA4C,MAAM,SAAS,CAAC;AA4DlF,MAAM,MAAM,QAAQ,GAAG;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,OAAO,GAAG,kBAAkB,GAAG,eAAe,GAAG,mBAAmB,CAAC;;oBAK7D,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC;wBAChD,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,CAAC;yBAC/C,CACnB,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC,KAC9C,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;yBACf,CAAC,CAAC,EACrB,IAAI,EAAE,gBAAgB,KACnB,MAAM,CAAC,MAAM,CAAC,iBAAiB,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC;4BACzB,MAAM,MAAM,CAAC,MAAM,CAAC,gBAAgB,GAAG,SAAS,EAAE,KAAK,CAAC;0BAC1D,MAAM,MAAM,CAAC,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC;6BACnC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAC3B,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,kBAAkB,EAAE,CAAC,CAAC;;AAjBpD,qBAAa,aAAc,SAAQ,kBAmBF;IAC/B,MAAM,CAAC,QAAQ,CAAC,KAAK,6FA0RnB;CACH"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/gh-tool/text-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"text-input.d.ts","sourceRoot":"","sources":["../../src/gh-tool/text-input.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAGxC,OAAO,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGhD,OAAO,EAAE,wBAAwB,EAAE,MAAM,0BAA0B,CAAC;AAepE,QAAA,MAAM,WAAW,wDAAgD,CAAC;AAIlE,eAAO,MAAM,eAAe,aAAc,MAAM,YACyC,CAAC;AAE1F,eAAO,MAAM,oBAAoB,SAAU,MAAM,WAAW,MAAM,0FAYjE,CAAC;AAEF,eAAO,MAAM,yBAAyB,aAAc,MAAM,WAAW,MAAM;;;6BAUH,CAAC;AAEzE,eAAO,MAAM,oBAAoB,aAAc,MAAM,WAAW,MAAM,mDACJ,CAAC;AAuBnE,KAAK,uBAAuB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,WAAW,CAAC,CAAC;IACpD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAwFF,eAAO,MAAM,wBAAwB,YAC1B,IAAI,CAAC,uBAAuB,EAAE,aAAa,GAAG,cAAc,CAAC,KACrE,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAIuD,CAAC;AAEnG,eAAO,MAAM,wBAAwB,YAC1B,IAAI,CAAC,uBAAuB,EAAE,aAAa,GAAG,cAAc,CAAC,KACrE,MAAM,CAAC,MAAM,CAAC,MAAM,GAAG,IAAI,EAAE,kBAAkB,CAI9C,CAAC;AAEL,eAAO,MAAM,uBAAuB,YACzB,IAAI,CAAC,uBAAuB,EAAE,aAAa,GAAG,cAAc,CAAC,GAAG;IACvE,YAAY,EAAE,MAAM,CAAC;CACtB,KACA,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAKsD,CAAC"}
|
package/package.json
CHANGED
|
@@ -478,7 +478,7 @@ function unwrapProofCommand(words: string[], marker: string): string[] | undefin
|
|
|
478
478
|
function isLiteralLoopCommand(words: string[], marker: string, value: string): boolean {
|
|
479
479
|
const marked = unwrapProofCommand(words, marker);
|
|
480
480
|
if (!marked) return false;
|
|
481
|
-
const argv = marked.map((word) => word.replaceAll(marker, value));
|
|
481
|
+
const argv = marked.map((word) => word.replaceAll(marker, () => value));
|
|
482
482
|
const name = argv[0];
|
|
483
483
|
const uses = marked.flatMap((word, index) => (word.includes(marker) ? [index] : []));
|
|
484
484
|
if (name === "cat")
|
|
@@ -579,7 +579,7 @@ function materializeLiteralShell(command: string): LiteralShellProof | undefined
|
|
|
579
579
|
);
|
|
580
580
|
if (size > 65_536) return undefined;
|
|
581
581
|
return {
|
|
582
|
-
command: values.map((value) => marked.text.replaceAll(marker, value)).join(";\n"),
|
|
582
|
+
command: values.map((value) => marked.text.replaceAll(marker, () => value)).join(";\n"),
|
|
583
583
|
paths: [],
|
|
584
584
|
};
|
|
585
585
|
}
|
|
@@ -621,8 +621,11 @@ function materializeLiteralShell(command: string): LiteralShellProof | undefined
|
|
|
621
621
|
(assignment[1]?.length ?? 0) +
|
|
622
622
|
7;
|
|
623
623
|
if (size > 65_536) return undefined;
|
|
624
|
-
const paths = argv.slice(3).map((word) => word.replaceAll(marker, value));
|
|
625
|
-
return {
|
|
624
|
+
const paths = argv.slice(3).map((word) => word.replaceAll(marker, () => value));
|
|
625
|
+
return {
|
|
626
|
+
command: `cd ${assignment[1]} && ${use.replaceAll(marker, () => value)}${suffix}`,
|
|
627
|
+
paths,
|
|
628
|
+
};
|
|
626
629
|
}
|
|
627
630
|
// Only the observed rg -n PATTERN PATH form; marker position proves a real file operand.
|
|
628
631
|
const brace =
|
|
@@ -662,7 +665,7 @@ function materializeLiteralShell(command: string): LiteralShellProof | undefined
|
|
|
662
665
|
new TextEncoder().encode(command.slice(0, start)).byteLength +
|
|
663
666
|
values.reduce((total, value) => total + path.length - members[0].length + value.length + 1, -1);
|
|
664
667
|
if (size > 65_536) return undefined;
|
|
665
|
-
const paths = values.map((member) => path.replace(members[0], member));
|
|
668
|
+
const paths = values.map((member) => path.replace(members[0], () => member));
|
|
666
669
|
return { command: command.slice(0, start) + paths.join(" "), paths };
|
|
667
670
|
}
|
|
668
671
|
|
|
@@ -770,7 +773,7 @@ function normalizeProgramHeredocs(command: string): string {
|
|
|
770
773
|
if (!/-\s*$/.test(heredoc.header)) return command;
|
|
771
774
|
const quoted = "'" + heredoc.body.replaceAll("'", "'\"'\"'") + "'";
|
|
772
775
|
return (
|
|
773
|
-
heredoc.header.replace(/-\s*$/, "-c " + quoted) +
|
|
776
|
+
heredoc.header.replace(/-\s*$/, () => "-c " + quoted) +
|
|
774
777
|
"\n" +
|
|
775
778
|
normalizeProgramHeredocs(heredoc.following)
|
|
776
779
|
);
|
|
@@ -987,7 +990,7 @@ function inlinePackageCommands(command: string, allowInventory: boolean): string
|
|
|
987
990
|
position === 2
|
|
988
991
|
? program.replace(
|
|
989
992
|
/\brequire\s*\(\s*(["'])\.\/\1\s*\+\s*[a-z]\s*\)/,
|
|
990
|
-
`require(${JSON.stringify(path)})`,
|
|
993
|
+
() => `require(${JSON.stringify(path)})`,
|
|
991
994
|
)
|
|
992
995
|
: word;
|
|
993
996
|
return "'" + materialized.replaceAll("'", "'\"'\"'") + "'";
|
|
@@ -997,18 +1000,6 @@ function inlinePackageCommands(command: string, allowInventory: boolean): string
|
|
|
997
1000
|
});
|
|
998
1001
|
}
|
|
999
1002
|
|
|
1000
|
-
/** JSON values selected by arbitrary keys or printed wholesale have no metadata proof. */
|
|
1001
|
-
function hasUnverifiedJsonValueOutput(code: string): boolean {
|
|
1002
|
-
if (!/\bjson\.loads?\s*\(/.test(code) || !/\bprint\s*\(/.test(code)) return false;
|
|
1003
|
-
if (/\.items\s*\(/.test(code) || /\bprint\s*\(\s*json\.loads?\s*\(/.test(code)) return true;
|
|
1004
|
-
const names = [...code.matchAll(/\b([A-Za-z_]\w*)\s*=\s*json\.loads?\s*\(/g)].map(
|
|
1005
|
-
(match) => match[1] ?? "",
|
|
1006
|
-
);
|
|
1007
|
-
return names.some((name) =>
|
|
1008
|
-
new RegExp(`\\bprint\\s*\\(\\s*(?:json\\.dumps\\s*\\(\\s*)?${name}\\s*[,)]`).test(code),
|
|
1009
|
-
);
|
|
1010
|
-
}
|
|
1011
|
-
|
|
1012
1003
|
function hasInlineProgramExecution(
|
|
1013
1004
|
program: string,
|
|
1014
1005
|
python: boolean,
|
|
@@ -1016,7 +1007,6 @@ function hasInlineProgramExecution(
|
|
|
1016
1007
|
): boolean {
|
|
1017
1008
|
const parts = programParts(program, python);
|
|
1018
1009
|
if (!parts) return true;
|
|
1019
|
-
if (python && hasUnverifiedJsonValueOutput(parts.code)) return true;
|
|
1020
1010
|
if (parts.loadedModules.some((name) => /^(?:node:)?vm$/.test(name))) return true;
|
|
1021
1011
|
// Dynamic loading and reflective access cannot establish a non-executing code operand.
|
|
1022
1012
|
if (
|
|
@@ -1842,7 +1832,7 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
1842
1832
|
if (invalidExpansion) return true;
|
|
1843
1833
|
|
|
1844
1834
|
const localMaterialized = [...localValues].reduce(
|
|
1845
|
-
(text, [marker, value]) => text.replaceAll(marker, value),
|
|
1835
|
+
(text, [marker, value]) => text.replaceAll(marker, () => value),
|
|
1846
1836
|
normalized,
|
|
1847
1837
|
);
|
|
1848
1838
|
// Options must be checked after local literals become actual argv, never as placeholders.
|
|
@@ -1900,7 +1890,7 @@ function hasEnvironmentVariableExpansionRead(
|
|
|
1900
1890
|
}
|
|
1901
1891
|
}
|
|
1902
1892
|
const materialized = [...localValues].reduce(
|
|
1903
|
-
(text, [marker, value]) => text.replaceAll(marker, value),
|
|
1893
|
+
(text, [marker, value]) => text.replaceAll(marker, () => value),
|
|
1904
1894
|
normalized.replaceAll("ENVVALUE", "SAFEENVVALUE"),
|
|
1905
1895
|
);
|
|
1906
1896
|
return hasSensitiveFileRead(materialized, isPathBlocked);
|
|
@@ -2137,7 +2127,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
2137
2127
|
const rawEnvironmentSource =
|
|
2138
2128
|
mentionsEnvironmentSource(command) && mentionsInlineRuntime(command);
|
|
2139
2129
|
if (rawEnvironmentSource && hasEnvironmentMutationPrefix(originalCommand))
|
|
2140
|
-
return "
|
|
2130
|
+
return "blocked by the environment/execution policy after an unproved environment mutation";
|
|
2141
2131
|
command = normalizeProgramHeredocs(command);
|
|
2142
2132
|
const proof = materializeLiteralShell(command);
|
|
2143
2133
|
if (!proof && /^\s*for\b/.test(command)) return "cannot prove a bounded literal shell loop";
|
|
@@ -2158,19 +2148,19 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
2158
2148
|
if (
|
|
2159
2149
|
hasSensitiveFileRead(command, isPathBlocked) ||
|
|
2160
2150
|
hasSensitivePathRedirect(command, isPathBlocked)
|
|
2161
|
-
)
|
|
2162
|
-
return "
|
|
2163
|
-
}
|
|
2151
|
+
)
|
|
2152
|
+
return "blocked by the file-access policy; a sensitive path matched or this command form could not be verified";
|
|
2164
2153
|
if (
|
|
2165
2154
|
!isLiteralTextWrite(command) &&
|
|
2166
2155
|
hasEnvironmentVariableExpansionRead(command, allowedEnvironmentVariables, isPathBlocked)
|
|
2167
2156
|
) {
|
|
2168
2157
|
return "expands an unapproved or executable environment variable value";
|
|
2169
2158
|
}
|
|
2170
|
-
if (
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2159
|
+
if (
|
|
2160
|
+
hasEnvironmentSourceAccess(command, rawEnvironmentSource) ||
|
|
2161
|
+
hasEnvironmentRead(command, allowedEnvironmentVariables, allowPackageInventory)
|
|
2162
|
+
) {
|
|
2163
|
+
return "blocked by the environment/execution policy; permitted access or non-execution could not be established for this command form";
|
|
2174
2164
|
}
|
|
2175
2165
|
if (
|
|
2176
2166
|
dangerousBashPatterns.some(
|
|
@@ -2263,7 +2253,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
2263
2253
|
if (error instanceof Error) {
|
|
2264
2254
|
if (sanitized !== message) {
|
|
2265
2255
|
error.message = sanitized;
|
|
2266
|
-
if (error.stack) error.stack = error.stack.replace(message, sanitized);
|
|
2256
|
+
if (error.stack) error.stack = error.stack.replace(message, () => sanitized);
|
|
2267
2257
|
}
|
|
2268
2258
|
throw error;
|
|
2269
2259
|
}
|
|
@@ -2319,7 +2309,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
2319
2309
|
throw new Error(
|
|
2320
2310
|
`\u{1F6AB} Command blocked: ${dangerousReason}; this command might expose secrets.\n\n` +
|
|
2321
2311
|
`Command: ${command}\n\n` +
|
|
2322
|
-
`
|
|
2312
|
+
`Use a supported command form or the appropriate wrapper tool.\n\n` +
|
|
2323
2313
|
`Think this is wrong? Review the project repository, adjust the patterns, and submit a PR.`,
|
|
2324
2314
|
);
|
|
2325
2315
|
}
|
package/src/gh-tool/api.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { retryTransient } from "#shared/retry-transient";
|
|
|
4
4
|
|
|
5
5
|
import { GitHubAuthError, GitHubCommandError, GitHubNotFoundError } from "./errors";
|
|
6
6
|
import type { GitHubApiError } from "./errors";
|
|
7
|
+
import { ghEnvironment } from "./environment";
|
|
7
8
|
import { validateOutboundText } from "./text-input";
|
|
8
9
|
|
|
9
10
|
// Direct HTTP, not `gh api`: the CLI collapses every failure into a non-zero exit and
|
|
@@ -44,6 +45,7 @@ export const resolveGitHubToken = Effect.fn("gh.resolveGitHubToken")(function* (
|
|
|
44
45
|
const proc = Bun.spawn(["gh", "auth", "token", "--hostname", "github.com"], {
|
|
45
46
|
stdout: "pipe",
|
|
46
47
|
stderr: "ignore",
|
|
48
|
+
env: ghEnvironment(),
|
|
47
49
|
});
|
|
48
50
|
const stdout = await new Response(proc.stdout).text();
|
|
49
51
|
const exitCode = await proc.exited;
|
|
@@ -67,27 +69,22 @@ export type GitHubApiRequest = {
|
|
|
67
69
|
|
|
68
70
|
const MAX_API_RETRIES = 2;
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
// Traverse only parsed JSON, never the caller's getters or toJSON hooks.
|
|
73
|
+
const decodedBodyText = (value: unknown): string => {
|
|
71
74
|
if (typeof value === "string") return value;
|
|
72
|
-
if (Array.isArray(value)) return value.map(
|
|
75
|
+
if (Array.isArray(value)) return value.map(decodedBodyText).join("\n");
|
|
73
76
|
if (value !== null && typeof value === "object")
|
|
74
|
-
return Object.
|
|
77
|
+
return Object.entries(value)
|
|
78
|
+
.map(([key, item]) => `${key}\n${decodedBodyText(item)}`)
|
|
79
|
+
.join("\n");
|
|
75
80
|
return "";
|
|
76
81
|
};
|
|
77
82
|
|
|
78
|
-
const githubApiAttempt = Effect.fn("gh.githubApiAttempt")(function* <T>(
|
|
83
|
+
const githubApiAttempt = Effect.fn("gh.githubApiAttempt")(function* <T>(
|
|
84
|
+
opts: GitHubApiRequest,
|
|
85
|
+
serializedBody: string | undefined,
|
|
86
|
+
) {
|
|
79
87
|
const method = opts.method ?? "GET";
|
|
80
|
-
const bodyText = yield* Effect.try({
|
|
81
|
-
try: () => outboundBodyText(opts.body),
|
|
82
|
-
catch: () =>
|
|
83
|
-
new GitHubCommandError({
|
|
84
|
-
command: `gh-tool ${method} ${opts.path}`,
|
|
85
|
-
exitCode: 1,
|
|
86
|
-
stderr: "Refusing to publish an invalid request body",
|
|
87
|
-
message: "Refusing to publish an invalid request body",
|
|
88
|
-
}),
|
|
89
|
-
});
|
|
90
|
-
yield* validateOutboundText(`${opts.path}\n${bodyText}`, `gh-tool ${method} ${opts.path}`);
|
|
91
88
|
const token = yield* resolveGitHubToken();
|
|
92
89
|
const url = `${GITHUB_API_ROOT}/${opts.path.replace(/^\//, "")}`;
|
|
93
90
|
|
|
@@ -99,9 +96,9 @@ const githubApiAttempt = Effect.fn("gh.githubApiAttempt")(function* <T>(opts: Gi
|
|
|
99
96
|
Accept: GITHUB_ACCEPT,
|
|
100
97
|
Authorization: `Bearer ${token}`,
|
|
101
98
|
"X-GitHub-Api-Version": GITHUB_API_VERSION,
|
|
102
|
-
...(
|
|
99
|
+
...(serializedBody === undefined ? {} : { "Content-Type": "application/json" }),
|
|
103
100
|
},
|
|
104
|
-
...(
|
|
101
|
+
...(serializedBody === undefined ? {} : { body: serializedBody }),
|
|
105
102
|
}),
|
|
106
103
|
catch: (cause) =>
|
|
107
104
|
new GitHubCommandError({
|
|
@@ -158,13 +155,42 @@ const githubApiAttempt = Effect.fn("gh.githubApiAttempt")(function* <T>(opts: Gi
|
|
|
158
155
|
export const githubApi = <T>(
|
|
159
156
|
opts: GitHubApiRequest,
|
|
160
157
|
): Effect.Effect<GitHubApiResponse<T>, GitHubApiError> =>
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
158
|
+
Effect.gen(function* () {
|
|
159
|
+
const snapshot = yield* Effect.try({
|
|
160
|
+
try: () => {
|
|
161
|
+
const path = opts.path;
|
|
162
|
+
const method = opts.method ?? "GET";
|
|
163
|
+
const alsoAcceptStatus = opts.alsoAcceptStatus;
|
|
164
|
+
const request = {
|
|
165
|
+
path,
|
|
166
|
+
method,
|
|
167
|
+
...(alsoAcceptStatus === undefined ? {} : { alsoAcceptStatus: [...alsoAcceptStatus] }),
|
|
168
|
+
};
|
|
169
|
+
const body = opts.body;
|
|
170
|
+
if (body === undefined) return { request, serialized: undefined, decoded: "" };
|
|
171
|
+
const serialized = JSON.stringify(body);
|
|
172
|
+
if (serialized === undefined) throw new Error("Body is not JSON serializable");
|
|
173
|
+
return { request, serialized, decoded: decodedBodyText(JSON.parse(serialized)) };
|
|
174
|
+
},
|
|
175
|
+
catch: () =>
|
|
176
|
+
new GitHubCommandError({
|
|
177
|
+
command: "gh-tool API",
|
|
178
|
+
exitCode: 1,
|
|
179
|
+
stderr: "Refusing to publish an invalid request body",
|
|
180
|
+
message: "Refusing to publish an invalid request body",
|
|
181
|
+
}),
|
|
182
|
+
});
|
|
183
|
+
const { request } = snapshot;
|
|
184
|
+
yield* validateOutboundText(
|
|
185
|
+
`${request.path}\n${snapshot.serialized ?? ""}\n${snapshot.decoded}`,
|
|
186
|
+
`gh-tool ${request.method} ${request.path}`,
|
|
187
|
+
);
|
|
188
|
+
return yield* retryTransient({
|
|
189
|
+
attempt: () => githubApiAttempt<T>(request, snapshot.serialized),
|
|
190
|
+
isTransient: (error) =>
|
|
191
|
+
error._tag === "GitHubCommandError" && error.retryable === true && request.method === "GET",
|
|
192
|
+
maxRetries: MAX_API_RETRIES,
|
|
193
|
+
});
|
|
168
194
|
});
|
|
169
195
|
|
|
170
196
|
const safeJsonParse = (text: string): unknown => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Exact names only: GH children need their own auth/config, not unrelated credentials
|
|
2
|
+
// or executable-loader, editor, and pager settings from the parent process.
|
|
3
|
+
const GH_ENVIRONMENT_NAMES = [
|
|
4
|
+
"PATH",
|
|
5
|
+
"HOME",
|
|
6
|
+
"USERPROFILE",
|
|
7
|
+
"APPDATA",
|
|
8
|
+
"AppData",
|
|
9
|
+
"HOMEDRIVE",
|
|
10
|
+
"HOMEPATH",
|
|
11
|
+
"SYSTEMROOT",
|
|
12
|
+
"SystemRoot",
|
|
13
|
+
"WINDIR",
|
|
14
|
+
"TMPDIR",
|
|
15
|
+
"TMP",
|
|
16
|
+
"TEMP",
|
|
17
|
+
"LANG",
|
|
18
|
+
"LC_ALL",
|
|
19
|
+
"LC_CTYPE",
|
|
20
|
+
"TERM",
|
|
21
|
+
"XDG_CONFIG_HOME",
|
|
22
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
23
|
+
"GH_CONFIG_DIR",
|
|
24
|
+
"GH_HOST",
|
|
25
|
+
"GH_REPO",
|
|
26
|
+
"GH_TOKEN",
|
|
27
|
+
"GITHUB_TOKEN",
|
|
28
|
+
"GH_ENTERPRISE_TOKEN",
|
|
29
|
+
"GITHUB_ENTERPRISE_TOKEN",
|
|
30
|
+
"HTTP_PROXY",
|
|
31
|
+
"http_proxy",
|
|
32
|
+
"HTTPS_PROXY",
|
|
33
|
+
"https_proxy",
|
|
34
|
+
"ALL_PROXY",
|
|
35
|
+
"all_proxy",
|
|
36
|
+
"NO_PROXY",
|
|
37
|
+
"no_proxy",
|
|
38
|
+
"SSL_CERT_FILE",
|
|
39
|
+
"SSL_CERT_DIR",
|
|
40
|
+
] as const;
|
|
41
|
+
|
|
42
|
+
export const ghEnvironment = (
|
|
43
|
+
environment: Record<string, string | undefined> = process.env,
|
|
44
|
+
): Record<string, string> =>
|
|
45
|
+
Object.fromEntries(
|
|
46
|
+
GH_ENVIRONMENT_NAMES.flatMap((name) => {
|
|
47
|
+
const value = environment[name];
|
|
48
|
+
return value === undefined ? [] : [[name, value]];
|
|
49
|
+
}),
|
|
50
|
+
);
|
package/src/gh-tool/service.ts
CHANGED
|
@@ -5,6 +5,7 @@ import type { GitHubRepoConfig } from "#config";
|
|
|
5
5
|
import type { RepoInfo } from "./types";
|
|
6
6
|
|
|
7
7
|
import { GH_BINARY } from "./config";
|
|
8
|
+
import { ghEnvironment } from "./environment";
|
|
8
9
|
import { GitHubAuthError, GitHubCommandError, GitHubNotFoundError } from "./errors";
|
|
9
10
|
import { retryTransient } from "#shared/retry-transient";
|
|
10
11
|
import { githubApi } from "./api";
|
|
@@ -165,7 +166,8 @@ export class GitHubService extends Context.Service<
|
|
|
165
166
|
const command = ChildProcess.make(GH_BINARY, args, {
|
|
166
167
|
stdout: "pipe",
|
|
167
168
|
stderr: "pipe",
|
|
168
|
-
...(ghRepo ? {
|
|
169
|
+
env: { ...ghEnvironment(), ...(ghRepo ? { GH_REPO: ghRepo } : {}) },
|
|
170
|
+
extendEnv: false,
|
|
169
171
|
});
|
|
170
172
|
|
|
171
173
|
const proc = yield* executor.spawn(command);
|
|
@@ -12,13 +12,19 @@ const SENSITIVE_PATH_PATTERNS = [
|
|
|
12
12
|
/\.envrc$/,
|
|
13
13
|
/\.(pem|key|p12|pfx|cer|crt)$/i,
|
|
14
14
|
/(?:^|[\\/])(credentials?|passwd|shadow)$/i,
|
|
15
|
+
/(?:^|\/)(?:\.ssh|\.aws|\.kube|\.azure|secrets?|credentials?)(?:\/|$)/i,
|
|
16
|
+
/(?:^|\/)(?:\.npmrc|\.netrc|\.git-credentials|\.sentryclirc|\.pypirc|\.pgpass)$/i,
|
|
17
|
+
/(?:^|\/)\.docker\/config\.json$/i,
|
|
18
|
+
/(?:^|\/)\.config\/gh\/hosts\.yml$/i,
|
|
19
|
+
/(?:^|\/)GitHub CLI\/hosts\.yml$/i,
|
|
20
|
+
/(?:^|\/)(?:kubeconfig(?:[.-].+)?|[^/]+\.kubeconfig)$/i,
|
|
15
21
|
];
|
|
16
22
|
const MissingMode = Schema.Literals(["error", "null", "default"]);
|
|
17
23
|
|
|
18
24
|
const readTextFromStdin = () => Bun.stdin.text();
|
|
19
25
|
|
|
20
26
|
export const isSensitivePath = (filePath: string) =>
|
|
21
|
-
SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(filePath));
|
|
27
|
+
SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(filePath.replaceAll("\\", "/")));
|
|
22
28
|
|
|
23
29
|
export const validateOutboundText = (text: string, command: string) => {
|
|
24
30
|
const reason = unsafeOutboundTextReason(text);
|
package/src/shared/bun.ts
CHANGED