@getmarrow/mcp 3.9.108 → 3.9.110
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 +26 -1
- package/dist/cli.js +44 -3
- package/dist/cli.js.map +1 -1
- package/dist/control-notice.d.ts +13 -0
- package/dist/control-notice.d.ts.map +1 -0
- package/dist/control-notice.js +76 -0
- package/dist/control-notice.js.map +1 -0
- package/dist/control-routes.d.ts +10 -0
- package/dist/control-routes.d.ts.map +1 -0
- package/dist/control-routes.js +81 -0
- package/dist/control-routes.js.map +1 -0
- package/dist/env.d.ts +15 -0
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +29 -2
- package/dist/env.js.map +1 -1
- package/dist/hook-context.d.ts.map +1 -1
- package/dist/hook-context.js +34 -11
- package/dist/hook-context.js.map +1 -1
- package/dist/hook-contract.d.ts +2 -2
- package/dist/hook-contract.js +1 -1
- package/dist/hook-pre-action.d.ts +10 -2
- package/dist/hook-pre-action.d.ts.map +1 -1
- package/dist/hook-pre-action.js +105 -33
- package/dist/hook-pre-action.js.map +1 -1
- package/dist/hook-session.d.ts.map +1 -1
- package/dist/hook-session.js +12 -7
- package/dist/hook-session.js.map +1 -1
- package/dist/hook-tool-policy.d.ts +1 -1
- package/dist/hook-tool-policy.d.ts.map +1 -1
- package/dist/hook.d.ts.map +1 -1
- package/dist/hook.js +19 -14
- package/dist/hook.js.map +1 -1
- package/dist/host-approval.d.ts +9 -0
- package/dist/host-approval.d.ts.map +1 -1
- package/dist/host-approval.js +22 -0
- package/dist/host-approval.js.map +1 -1
- package/dist/identity-explain.d.ts +35 -0
- package/dist/identity-explain.d.ts.map +1 -0
- package/dist/identity-explain.js +60 -0
- package/dist/identity-explain.js.map +1 -0
- package/dist/ipv4-first.d.ts +7 -0
- package/dist/ipv4-first.d.ts.map +1 -0
- package/dist/ipv4-first.js +60 -0
- package/dist/ipv4-first.js.map +1 -0
- package/dist/key-class.d.ts +16 -0
- package/dist/key-class.d.ts.map +1 -0
- package/dist/key-class.js +37 -0
- package/dist/key-class.js.map +1 -0
- package/dist/keyless-notice.d.ts +10 -0
- package/dist/keyless-notice.d.ts.map +1 -0
- package/dist/keyless-notice.js +47 -0
- package/dist/keyless-notice.js.map +1 -0
- package/dist/normalized-action.d.ts +4 -0
- package/dist/normalized-action.d.ts.map +1 -1
- package/dist/normalized-action.js +60 -27
- package/dist/normalized-action.js.map +1 -1
- package/dist/own-server-identity.d.ts +20 -0
- package/dist/own-server-identity.d.ts.map +1 -1
- package/dist/own-server-identity.js +130 -5
- package/dist/own-server-identity.js.map +1 -1
- package/dist/owner-bypass.d.ts +9 -0
- package/dist/owner-bypass.d.ts.map +1 -0
- package/dist/owner-bypass.js +79 -0
- package/dist/owner-bypass.js.map +1 -0
- package/dist/redact.d.ts +5 -0
- package/dist/redact.d.ts.map +1 -1
- package/dist/redact.js +61 -9
- package/dist/redact.js.map +1 -1
- package/dist/seatbelt/hook.d.ts +3 -1
- package/dist/seatbelt/hook.d.ts.map +1 -1
- package/dist/seatbelt/hook.js +7 -3
- package/dist/seatbelt/hook.js.map +1 -1
- package/dist/seatbelt/mapper.d.ts.map +1 -1
- package/dist/seatbelt/mapper.js +849 -12
- package/dist/seatbelt/mapper.js.map +1 -1
- package/dist/seatbelt/patch-text.d.ts +49 -0
- package/dist/seatbelt/patch-text.d.ts.map +1 -0
- package/dist/seatbelt/patch-text.js +376 -0
- package/dist/seatbelt/patch-text.js.map +1 -0
- package/dist/seatbelt/tables.d.ts +66 -0
- package/dist/seatbelt/tables.d.ts.map +1 -1
- package/dist/seatbelt/tables.js +467 -3
- package/dist/seatbelt/tables.js.map +1 -1
- package/dist/seatbelt/win-protected.d.ts +52 -0
- package/dist/seatbelt/win-protected.d.ts.map +1 -0
- package/dist/seatbelt/win-protected.js +1279 -0
- package/dist/seatbelt/win-protected.js.map +1 -0
- package/dist/self-protect.d.ts +18 -0
- package/dist/self-protect.d.ts.map +1 -0
- package/dist/self-protect.js +185 -0
- package/dist/self-protect.js.map +1 -0
- package/dist/session-id.d.ts +2 -0
- package/dist/session-id.d.ts.map +1 -0
- package/dist/session-id.js +32 -0
- package/dist/session-id.js.map +1 -0
- package/dist/session-loop-guard.d.ts +40 -0
- package/dist/session-loop-guard.d.ts.map +1 -1
- package/dist/session-loop-guard.js +236 -9
- package/dist/session-loop-guard.js.map +1 -1
- package/dist/session-marker.d.ts +7 -0
- package/dist/session-marker.d.ts.map +1 -0
- package/dist/session-marker.js +42 -0
- package/dist/session-marker.js.map +1 -0
- package/dist/win-private-state.d.ts +117 -0
- package/dist/win-private-state.d.ts.map +1 -0
- package/dist/win-private-state.js +544 -0
- package/dist/win-private-state.js.map +1 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.preferIpv4FirstWithHappyEyeballs = preferIpv4FirstWithHappyEyeballs;
|
|
37
|
+
const dns = __importStar(require("node:dns"));
|
|
38
|
+
const net = __importStar(require("node:net"));
|
|
39
|
+
let applied = false;
|
|
40
|
+
/**
|
|
41
|
+
* N1: the pre-action hook dials the Marrow API with IPv4 tried first and happy eyeballs on, so an IPv6 address that does not
|
|
42
|
+
* route (the resolver lists it first) cannot hold the hook past the host's budget. Transport only: the host, port, path and
|
|
43
|
+
* headers of every request are unchanged. Set once, before the first request; an older Node without either setting skips it.
|
|
44
|
+
*/
|
|
45
|
+
function preferIpv4FirstWithHappyEyeballs() {
|
|
46
|
+
if (applied)
|
|
47
|
+
return;
|
|
48
|
+
applied = true;
|
|
49
|
+
try {
|
|
50
|
+
if (typeof dns.setDefaultResultOrder === 'function')
|
|
51
|
+
dns.setDefaultResultOrder('ipv4first');
|
|
52
|
+
}
|
|
53
|
+
catch { /* older Node */ }
|
|
54
|
+
try {
|
|
55
|
+
if (typeof net.setDefaultAutoSelectFamily === 'function')
|
|
56
|
+
net.setDefaultAutoSelectFamily(true);
|
|
57
|
+
}
|
|
58
|
+
catch { /* older Node */ }
|
|
59
|
+
}
|
|
60
|
+
//# sourceMappingURL=ipv4-first.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ipv4-first.js","sourceRoot":"","sources":["../src/ipv4-first.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAUA,4EAKC;AAfD,8CAAgC;AAChC,8CAAgC;AAEhC,IAAI,OAAO,GAAG,KAAK,CAAC;AAEpB;;;;GAIG;AACH,SAAgB,gCAAgC;IAC9C,IAAI,OAAO;QAAE,OAAO;IACpB,OAAO,GAAG,IAAI,CAAC;IACf,IAAI,CAAC;QAAC,IAAI,OAAO,GAAG,CAAC,qBAAqB,KAAK,UAAU;YAAE,GAAG,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,gBAAgB,CAAC,CAAC;IAC/H,IAAI,CAAC;QAAC,IAAI,OAAO,GAAG,CAAC,0BAA0B,KAAK,UAAU;YAAE,GAAG,CAAC,0BAA0B,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAAC,MAAM,CAAC,CAAC,gBAAgB,CAAC,CAAC;AACpI,CAAC"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The class (live or test) of a Stripe-style key (`sk_`, `rk_`, `pk_`) inside a span that a redactor is about to
|
|
3
|
+
* replace, so the marker that stands for the span keeps the class: an approval, a hold or a correlation made for an
|
|
4
|
+
* action with a test key can never cover the same action with a live key.
|
|
5
|
+
*
|
|
6
|
+
* Fail closed: the answer is `test` only when every key-shaped token of the span is the exact lowercase ASCII
|
|
7
|
+
* `sk_test_` / `rk_test_` / `pk_test_` shape. A live prefix in any spelling the text can be folded to (case, a
|
|
8
|
+
* `-` for the `_`, NFKC/NFKD-foldable forms, zero-width / format / combining characters, percent or Basic-auth base64
|
|
9
|
+
* encoding), an unknown or malformed prefix, or a test prefix that is not exactly canonical, is `live`. A span with
|
|
10
|
+
* no such token has no class (`null`): its marker stays what it was. The class is read, never copied: callers pick a
|
|
11
|
+
* marker from a closed set of constants.
|
|
12
|
+
*/
|
|
13
|
+
export type KeyClass = 'live' | 'test';
|
|
14
|
+
/** The class of the Stripe-style key(s) in `span`, or null when it holds none. Linear in the span. */
|
|
15
|
+
export declare function keyClassOf(span: string): KeyClass | null;
|
|
16
|
+
//# sourceMappingURL=key-class.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-class.d.ts","sourceRoot":"","sources":["../src/key-class.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,MAAM,CAAC;AAsBvC,sGAAsG;AACtG,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,IAAI,CASxD"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.keyClassOf = keyClassOf;
|
|
4
|
+
/** Format, combining and control characters (zero-width space and joiners, BOM, soft hyphen, bidi marks, accents, NUL). */
|
|
5
|
+
const HIDDEN = /[\p{Cf}\p{Mn}\p{Me}\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu;
|
|
6
|
+
const LIVE_TOKEN = /(?<![a-z0-9])(?:sk|rk|pk)[_-]live/;
|
|
7
|
+
/** `sk_` and more that is neither `test` nor `live`: an unknown or malformed class (`sk_prod_…`, `sk_…`). */
|
|
8
|
+
const UNKNOWN_TOKEN = /(?<![a-z0-9])(?:sk|rk|pk)_(?!test)[a-z0-9_-]{12,}/;
|
|
9
|
+
const TEST_TOKEN = /(?<![a-z0-9])(?:sk|rk|pk)[_-]test/g;
|
|
10
|
+
const CANONICAL_TEST = /(?<![A-Za-z0-9])(?:sk|rk|pk)_test_[A-Za-z0-9_-]{12,}/g;
|
|
11
|
+
const PLAIN_ASCII = /^[\t\n\r\x20-\x7e]*$/;
|
|
12
|
+
const BASE64_SPAN = /^\s*(?:Basic\s+)?([A-Za-z0-9+/]{16,4096}={0,2})\s*$/i;
|
|
13
|
+
const PERCENT = /%([0-9A-Fa-f]{2})/g;
|
|
14
|
+
/** The span with the readings a backend or a client may give it added after it (percent escapes, Basic-auth base64). */
|
|
15
|
+
function withReadings(span) {
|
|
16
|
+
let probe = span;
|
|
17
|
+
if (span.includes('%'))
|
|
18
|
+
probe += `\n${span.replace(PERCENT, (_m, hex) => String.fromCharCode(parseInt(hex, 16)))}`;
|
|
19
|
+
const basic = BASE64_SPAN.exec(span);
|
|
20
|
+
if (basic)
|
|
21
|
+
probe += `\n${Buffer.from(basic[1], 'base64').toString('latin1')}`;
|
|
22
|
+
return probe;
|
|
23
|
+
}
|
|
24
|
+
/** The class of the Stripe-style key(s) in `span`, or null when it holds none. Linear in the span. */
|
|
25
|
+
function keyClassOf(span) {
|
|
26
|
+
const probe = withReadings(span);
|
|
27
|
+
// NFKD, not NFKC: an accent composed onto a letter (`i` + U+0301) must come apart so the mark can be dropped.
|
|
28
|
+
const folded = probe.normalize('NFKD').toLowerCase().replace(HIDDEN, '');
|
|
29
|
+
if (LIVE_TOKEN.test(folded) || UNKNOWN_TOKEN.test(folded))
|
|
30
|
+
return 'live';
|
|
31
|
+
const tests = folded.match(TEST_TOKEN);
|
|
32
|
+
if (!tests)
|
|
33
|
+
return null;
|
|
34
|
+
const canonical = probe.match(CANONICAL_TEST);
|
|
35
|
+
return PLAIN_ASCII.test(probe) && canonical !== null && canonical.length === tests.length ? 'test' : 'live';
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=key-class.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"key-class.js","sourceRoot":"","sources":["../src/key-class.ts"],"names":[],"mappings":";;AAmCA,gCASC;AA9BD,2HAA2H;AAC3H,MAAM,MAAM,GAAG,oEAAoE,CAAC;AACpF,MAAM,UAAU,GAAG,mCAAmC,CAAC;AACvD,6GAA6G;AAC7G,MAAM,aAAa,GAAG,mDAAmD,CAAC;AAC1E,MAAM,UAAU,GAAG,oCAAoC,CAAC;AACxD,MAAM,cAAc,GAAG,uDAAuD,CAAC;AAC/E,MAAM,WAAW,GAAG,sBAAsB,CAAC;AAC3C,MAAM,WAAW,GAAG,sDAAsD,CAAC;AAC3E,MAAM,OAAO,GAAG,oBAAoB,CAAC;AAErC,wHAAwH;AACxH,SAAS,YAAY,CAAC,IAAY;IAChC,IAAI,KAAK,GAAG,IAAI,CAAC;IACjB,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,KAAK,IAAI,KAAK,IAAI,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE,GAAW,EAAE,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IAC3H,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACrC,IAAI,KAAK;QAAE,KAAK,IAAI,KAAK,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;IAC9E,OAAO,KAAK,CAAC;AACf,CAAC;AAED,sGAAsG;AACtG,SAAgB,UAAU,CAAC,IAAY;IACrC,MAAM,KAAK,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;IACjC,8GAA8G;IAC9G,MAAM,MAAM,GAAG,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACzE,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC;QAAE,OAAO,MAAM,CAAC;IACzE,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IACvC,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IACxB,MAAM,SAAS,GAAG,KAAK,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;IAC9C,OAAO,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,CAAC,MAAM,KAAK,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;AAC9G,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP-5: a hook that runs with local control on and no API key records nothing, so nothing tells the person that holds and approvals are
|
|
3
|
+
* not being recorded. This puts one user-visible notice per host session through the host's `systemMessage` field: the first keyless hook
|
|
4
|
+
* call of a session (a local marker keyed by a hash of the session id, mode 600, says it was sent) adds it to the hook's own JSON output,
|
|
5
|
+
* or prints it alone when the hook prints nothing. Names only: no variable value is ever read into the text. Claude Code only, the one
|
|
6
|
+
* host whose hook output carries `systemMessage` for every event; other hosts keep their exact output.
|
|
7
|
+
*/
|
|
8
|
+
export declare const KEYLESS_NOTICE_TEXT = "Marrow hooks in this session have no API key, so holds and approvals are NOT recorded. Put MARROW_API_KEY in ~/.marrow/env (chmod 600) or start the host with it, then restart the session.";
|
|
9
|
+
export declare function armKeylessNotice(harness: string, sessionId: unknown): boolean;
|
|
10
|
+
//# sourceMappingURL=keyless-notice.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyless-notice.d.ts","sourceRoot":"","sources":["../src/keyless-notice.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,eAAO,MAAM,mBAAmB,gMAAgM,CAAC;AAIjO,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,GAAG,OAAO,CAyB7E"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.KEYLESS_NOTICE_TEXT = void 0;
|
|
4
|
+
exports.armKeylessNotice = armKeylessNotice;
|
|
5
|
+
const session_marker_1 = require("./session-marker");
|
|
6
|
+
/**
|
|
7
|
+
* MCP-5: a hook that runs with local control on and no API key records nothing, so nothing tells the person that holds and approvals are
|
|
8
|
+
* not being recorded. This puts one user-visible notice per host session through the host's `systemMessage` field: the first keyless hook
|
|
9
|
+
* call of a session (a local marker keyed by a hash of the session id, mode 600, says it was sent) adds it to the hook's own JSON output,
|
|
10
|
+
* or prints it alone when the hook prints nothing. Names only: no variable value is ever read into the text. Claude Code only, the one
|
|
11
|
+
* host whose hook output carries `systemMessage` for every event; other hosts keep their exact output.
|
|
12
|
+
*/
|
|
13
|
+
exports.KEYLESS_NOTICE_TEXT = 'Marrow hooks in this session have no API key, so holds and approvals are NOT recorded. Put MARROW_API_KEY in ~/.marrow/env (chmod 600) or start the host with it, then restart the session.';
|
|
14
|
+
let armed = false;
|
|
15
|
+
function armKeylessNotice(harness, sessionId) {
|
|
16
|
+
if (armed || harness !== 'claude-code' || typeof sessionId !== 'string' || !sessionId)
|
|
17
|
+
return false;
|
|
18
|
+
if (!(0, session_marker_1.claimSessionMarker)('keyless-notice', sessionId))
|
|
19
|
+
return false;
|
|
20
|
+
armed = true;
|
|
21
|
+
const write = process.stdout.write.bind(process.stdout);
|
|
22
|
+
let carried = false;
|
|
23
|
+
process.stdout.write = ((chunk, ...rest) => {
|
|
24
|
+
if (!carried && typeof chunk === 'string' && chunk.trim().startsWith('{')) {
|
|
25
|
+
try {
|
|
26
|
+
const parsed = JSON.parse(chunk);
|
|
27
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && !('systemMessage' in parsed)) {
|
|
28
|
+
carried = true;
|
|
29
|
+
return write(JSON.stringify({ ...parsed, systemMessage: exports.KEYLESS_NOTICE_TEXT }), ...rest);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch { /* not JSON: written unchanged */ }
|
|
33
|
+
}
|
|
34
|
+
return write(chunk, ...rest);
|
|
35
|
+
});
|
|
36
|
+
process.on('exit', () => {
|
|
37
|
+
if (!carried) {
|
|
38
|
+
carried = true;
|
|
39
|
+
try {
|
|
40
|
+
write(JSON.stringify({ systemMessage: exports.KEYLESS_NOTICE_TEXT }));
|
|
41
|
+
}
|
|
42
|
+
catch { /* the notice is best effort */ }
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=keyless-notice.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"keyless-notice.js","sourceRoot":"","sources":["../src/keyless-notice.ts"],"names":[],"mappings":";;;AAaA,4CAyBC;AAtCD,qDAAsD;AAEtD;;;;;;GAMG;AACU,QAAA,mBAAmB,GAAG,6LAA6L,CAAC;AAEjO,IAAI,KAAK,GAAG,KAAK,CAAC;AAElB,SAAgB,gBAAgB,CAAC,OAAe,EAAE,SAAkB;IAClE,IAAI,KAAK,IAAI,OAAO,KAAK,aAAa,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IACpG,IAAI,CAAC,IAAA,mCAAkB,EAAC,gBAAgB,EAAE,SAAS,CAAC;QAAE,OAAO,KAAK,CAAC;IACnE,KAAK,GAAG,IAAI,CAAC;IACb,MAAM,KAAK,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAoD,CAAC;IAC3G,IAAI,OAAO,GAAG,KAAK,CAAC;IACpB,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC,KAAc,EAAE,GAAG,IAAe,EAAW,EAAE;QACtE,IAAI,CAAC,OAAO,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YAC1E,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAY,CAAC;gBAC5C,IAAI,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,eAAe,IAAI,MAAM,CAAC,EAAE,CAAC;oBACnG,OAAO,GAAG,IAAI,CAAC;oBACf,OAAO,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAI,MAAkC,EAAE,aAAa,EAAE,2BAAmB,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,CAAC;gBACxH,CAAC;YACH,CAAC;YAAC,MAAM,CAAC,CAAC,iCAAiC,CAAC,CAAC;QAC/C,CAAC;QACD,OAAO,KAAK,CAAC,KAAK,EAAE,GAAG,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAgC,CAAC;IAClC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE;QACtB,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,CAAC;gBAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,aAAa,EAAE,2BAAmB,EAAE,CAAC,CAAC,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,+BAA+B,CAAC,CAAC;QAClH,CAAC;IACH,CAAC,CAAC,CAAC;IACH,OAAO,IAAI,CAAC;AACd,CAAC"}
|
|
@@ -24,6 +24,10 @@
|
|
|
24
24
|
* `truncated: true`: Marrow cannot see a replacement inside the hash, and
|
|
25
25
|
* an approval is never reused for an action that carried a secret (it asks
|
|
26
26
|
* each time). The verdict, the hold and the prompt do not depend on it.
|
|
27
|
+
* A Stripe-style key (`sk_`, `rk_`, `pk_`) keeps its class in the marker it is replaced by: `[secret-live]` or
|
|
28
|
+
* `[secret-test]` (key-class.ts decides; an unknown, malformed or look-alike prefix is live), so an approval or hold
|
|
29
|
+
* made for an action with a test key never covers the same action with a live key. Any other credential stays
|
|
30
|
+
* `[secret]`. The class markers are three constants; the paths sent for an edit stay `[secret]`.
|
|
27
31
|
* Credentials are:
|
|
28
32
|
* values of environment assignments, flags, `NAME=value` arguments and
|
|
29
33
|
* inline `name: value` / `"name": "value"` literals whose name is a
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"normalized-action.d.ts","sourceRoot":"","sources":["../src/normalized-action.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"normalized-action.d.ts","sourceRoot":"","sources":["../src/normalized-action.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiFG;AACH,MAAM,MAAM,oBAAoB,GAAG;IACjC,SAAS,EAAE,OAAO,GAAG,MAAM,GAAG,KAAK,GAAG,OAAO,CAAC;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,UAAU,EAAE;QAAE,cAAc,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG;QAAE,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC7J,yGAAyG;IACzG,SAAS,CAAC,EAAE,IAAI,CAAC;CAClB,CAAC;AAWF,yHAAyH;AACzH,eAAO,MAAM,cAAc,QAAS,CAAC;AAWrC,KAAK,SAAS,GAAG;IAAE,SAAS,CAAC,EAAE,OAAO,CAAC;IAAC,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAqE/D,eAAO,MAAM,gBAAgB,QAAoZ,CAAC;AAelb,4GAA4G;AAC5G,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAUnD;AAgDD,KAAK,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,WAAW,GAAG,OAAO,CAAC;AAEpE;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAW1D;AAy/BD;;;;GAIG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,UAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,EAAE,OAAO,CAAA;CAAE,CAWjJ;AAwID;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAyChE;AAuCD,4HAA4H;AAC5H,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,SAAS,GAAG,MAAM,GAAG,IAAI,CAEtE;AA+MD;;;GAGG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAK7D;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAO,GAAG,oBAAoB,CA0CxI"}
|
|
@@ -11,7 +11,13 @@ exports.normalizedHookAction = normalizedHookAction;
|
|
|
11
11
|
const node_crypto_1 = require("node:crypto");
|
|
12
12
|
const node_os_1 = require("node:os");
|
|
13
13
|
const hook_tool_policy_1 = require("./hook-tool-policy");
|
|
14
|
+
const key_class_1 = require("./key-class");
|
|
14
15
|
const SECRET = '[secret]';
|
|
16
|
+
/** The marker of a replaced Stripe-style key keeps its class (a closed set: never built from input). */
|
|
17
|
+
const SECRET_LIVE = '[secret-live]';
|
|
18
|
+
const SECRET_TEST = '[secret-test]';
|
|
19
|
+
const SECRET_MARKER = /\[secret(?:-(?:live|test))?\]/;
|
|
20
|
+
const SECRET_MARKER_ALL = /\[secret-(?:live|test)\]/g;
|
|
15
21
|
const HASH_VERSION = 'marrow-normalized-action-v3';
|
|
16
22
|
const EDIT_TOOLS = /^(?:edit|write|multiedit|apply_patch|notebookedit|replace|write_file|edit_file|delete_file|search_replace|delete|create_file)$/i;
|
|
17
23
|
const MAX_DEPTH = 3;
|
|
@@ -36,9 +42,35 @@ function newExactness(deadline = Date.now() + DEFAULT_NORMALIZE_MS, exact = fals
|
|
|
36
42
|
* A value withheld only because it MAY be a secret (it may also be an id, a commit or a name that decides the
|
|
37
43
|
* action): replaced and the action marked truncated; the exact pass keeps it as it is.
|
|
38
44
|
*/
|
|
39
|
-
function withheld(state, original, replacement
|
|
45
|
+
function withheld(state, original, replacement) {
|
|
40
46
|
state.truncated = true; // exact: kept
|
|
41
|
-
return state.exact ? original : replacement;
|
|
47
|
+
return state.exact ? original : (replacement ?? secretFor(original));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The marker for a replaced span: `[secret-live]` / `[secret-test]` when the span holds a Stripe-style key of that class
|
|
51
|
+
* (key-class.ts: unknown, malformed and look-alike prefixes are live), else `[secret]`. A span past the scan limit is
|
|
52
|
+
* not read (it is withheld, truncated, and the exact pass keeps it as it is).
|
|
53
|
+
*/
|
|
54
|
+
function secretFor(span) {
|
|
55
|
+
if (span.length > exports.MAX_SCAN_CHARS)
|
|
56
|
+
return SECRET;
|
|
57
|
+
const keyClass = (0, key_class_1.keyClassOf)(span);
|
|
58
|
+
return keyClass === 'live' ? SECRET_LIVE : keyClass === 'test' ? SECRET_TEST : SECRET;
|
|
59
|
+
}
|
|
60
|
+
/** The marker for a replaced value of any type (a tool input field): an object or array is read as bounded JSON. */
|
|
61
|
+
function secretOfValue(value) {
|
|
62
|
+
if (typeof value === 'string')
|
|
63
|
+
return secretFor(value);
|
|
64
|
+
try {
|
|
65
|
+
return secretFor((JSON.stringify(value) ?? '').slice(0, exports.MAX_SCAN_CHARS));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return SECRET;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/** Whether a text holds a replaced-secret marker (any class). */
|
|
72
|
+
function hasSecretMarker(text) {
|
|
73
|
+
return SECRET_MARKER.test(text);
|
|
42
74
|
}
|
|
43
75
|
/** Counts text about to be scanned against the shared budget; past it, normalization stops. */
|
|
44
76
|
function spend(state, chars) {
|
|
@@ -94,7 +126,7 @@ function redactKeyShapes(text, state) {
|
|
|
94
126
|
if (whole && !/\s/.test(whole) && looksLikeKey(whole)) {
|
|
95
127
|
if (!exports.KNOWN_KEY_PREFIX.test(whole) && !PEM.test(whole))
|
|
96
128
|
return withheld(state, text);
|
|
97
|
-
return state.exact && PUBLIC_KNOWN_KEY.test(whole) ? text :
|
|
129
|
+
return state.exact && PUBLIC_KNOWN_KEY.test(whole) ? text : secretFor(whole);
|
|
98
130
|
}
|
|
99
131
|
if (PEM.test(text)) {
|
|
100
132
|
// The exact pass replaces only a private key block; a certificate or a public key block is kept.
|
|
@@ -105,7 +137,7 @@ function redactKeyShapes(text, state) {
|
|
|
105
137
|
return part;
|
|
106
138
|
if (!exports.KNOWN_KEY_PREFIX.test(part))
|
|
107
139
|
return withheld(state, part);
|
|
108
|
-
return state.exact && PUBLIC_KNOWN_KEY.test(part) ? part :
|
|
140
|
+
return state.exact && PUBLIC_KNOWN_KEY.test(part) ? part : secretFor(part);
|
|
109
141
|
}).join('');
|
|
110
142
|
}
|
|
111
143
|
// ---------------------------------------------------------------------------
|
|
@@ -236,10 +268,10 @@ function cutCredential(value, state) {
|
|
|
236
268
|
if (at >= value.length) {
|
|
237
269
|
if (holdsMoreThanCredential(value))
|
|
238
270
|
return withheld(state, value);
|
|
239
|
-
return
|
|
271
|
+
return secretFor(value);
|
|
240
272
|
}
|
|
241
273
|
state.truncated = true; // exact: certain (the credential before the field; the rest is scanned in the same pass)
|
|
242
|
-
return
|
|
274
|
+
return secretFor(value.slice(0, at)) + redactText(value.slice(at), state);
|
|
243
275
|
}
|
|
244
276
|
/** One-value auth header (`Authorization: Bearer X`, `X-Api-Key: K`): the whole value is the credential. */
|
|
245
277
|
const SINGLE_VALUE_HEADER = /^[^:\s]{1,128}:[ \t]*(?:(?:Bearer|Basic|Token|Bot)[ \t]+)?[^\s;,&]+[ \t]*$/i;
|
|
@@ -260,7 +292,7 @@ function redactText(text, state) {
|
|
|
260
292
|
// goes to host `deploy`), while libpq, mysql and redis clients take the whole span as the password. It is
|
|
261
293
|
// hidden either way, but it is not a certain credential: the action is marked truncated and the exact pass
|
|
262
294
|
// keeps the span, so it never shares a key with `https://deploy:<pw>@host`.
|
|
263
|
-
let out = text.replace(URL_CREDENTIALS, (_match, scheme, user, password) => (`${scheme}${user}:${/[#?]/.test(password) ? withheld(state, password) :
|
|
295
|
+
let out = text.replace(URL_CREDENTIALS, (_match, scheme, user, password) => (`${scheme}${user}:${/[#?]/.test(password) ? withheld(state, password) : secretFor(password)}@`));
|
|
264
296
|
// The exact pass keeps a `Token`/`Bot` value that is not key-shaped (`git commit -m "Token rotation"`).
|
|
265
297
|
out = replaceValuesAfter(out, AUTH_SCHEME, state.exact ? (scheme, value) => /^(?:Token|Bot)$/.test(scheme) && !looksLikeKey(value) : null);
|
|
266
298
|
// The exact pass keeps a URL `code` value: it may be an OAuth code, but it may as well be a country or product code.
|
|
@@ -286,7 +318,7 @@ function replaceValuesAfter(text, pattern, keep) {
|
|
|
286
318
|
pattern.lastIndex = end;
|
|
287
319
|
continue;
|
|
288
320
|
}
|
|
289
|
-
out += text.slice(copied, start) +
|
|
321
|
+
out += text.slice(copied, start) + secretFor(text.slice(start, end));
|
|
290
322
|
copied = end;
|
|
291
323
|
pattern.lastIndex = end;
|
|
292
324
|
}
|
|
@@ -326,7 +358,7 @@ function redactAssignments(text, state) {
|
|
|
326
358
|
continue;
|
|
327
359
|
const value = text.slice(start, end);
|
|
328
360
|
if (!value.startsWith('[secret') && !value.startsWith('[data:')) {
|
|
329
|
-
const replacement = kind === 'ambiguous' || holdsMoreThanCredential(value) ? withheld(state, value) :
|
|
361
|
+
const replacement = kind === 'ambiguous' || holdsMoreThanCredential(value) ? withheld(state, value) : secretFor(value);
|
|
330
362
|
out += text.slice(copied, start) + replacement;
|
|
331
363
|
copied = end;
|
|
332
364
|
}
|
|
@@ -349,7 +381,7 @@ function dataMarker(text, feedsSecrets, state, words = null) {
|
|
|
349
381
|
}
|
|
350
382
|
if (feedsSecrets) {
|
|
351
383
|
state.truncated = true; // exact: kept (hashed above, certain credentials replaced)
|
|
352
|
-
return
|
|
384
|
+
return secretFor(text);
|
|
353
385
|
}
|
|
354
386
|
// Secret-shaped data (`'{"api_key":"S","env":"production"}'`): the secrets are
|
|
355
387
|
// replaced and the rest is hashed, so data that differs outside a secret gives
|
|
@@ -573,7 +605,7 @@ function assignedValue(name, value, state) {
|
|
|
573
605
|
if (kind === 'credential' || kind === 'ambiguous') {
|
|
574
606
|
if (kind === 'ambiguous' || holdsMoreThanCredential(value))
|
|
575
607
|
return `${name}=${withheld(state, state.exact ? dataMarker(value, false, state) : value)}`;
|
|
576
|
-
return `${name}=${
|
|
608
|
+
return `${name}=${secretFor(value)}`;
|
|
577
609
|
}
|
|
578
610
|
return value ? `${name}=${dataMarker(value, false, state)}` : `${name}=`;
|
|
579
611
|
}
|
|
@@ -699,7 +731,7 @@ function redactSegment(words, ctx) {
|
|
|
699
731
|
const envStore = storeContext(lowered.slice(0, 4));
|
|
700
732
|
const stem = programStem(program);
|
|
701
733
|
const redactAt = (i, ambiguous = false) => {
|
|
702
|
-
out[i] = ambiguous || holdsMoreThanCredential(out[i]) ? withheld(state, out[i]) :
|
|
734
|
+
out[i] = ambiguous || holdsMoreThanCredential(out[i]) ? withheld(state, out[i]) : secretFor(out[i]);
|
|
703
735
|
handled.add(i);
|
|
704
736
|
};
|
|
705
737
|
// An auth header: a one-value header is the credential; any other (`Cookie: a=1; env=prod`)
|
|
@@ -711,7 +743,7 @@ function redactSegment(words, ctx) {
|
|
|
711
743
|
if (holdsMoreThanCredential(value))
|
|
712
744
|
return withheld(state, header);
|
|
713
745
|
if (!state.exact)
|
|
714
|
-
return
|
|
746
|
+
return secretFor(value);
|
|
715
747
|
// The exact pass: the value of a credential header (or a Bearer/Basic value) is certain; any other
|
|
716
748
|
// one-value header (`X-Idempotency-Key: k`) and the header name are kept.
|
|
717
749
|
const name = header.slice(0, colon);
|
|
@@ -720,7 +752,7 @@ function redactSegment(words, ctx) {
|
|
|
720
752
|
// Basic USER:PASSWORD names who acts, as `curl -u` does: only the password is certain.
|
|
721
753
|
const basic = /^[ \t]*Basic[ \t]+([A-Za-z0-9+/]+={0,2})[ \t]*$/i.exec(value);
|
|
722
754
|
const decoded = basic ? Buffer.from(basic[1], 'base64').toString('latin1') : '';
|
|
723
|
-
return /^[\x20-\x7e]*:/.test(decoded) ? `${name}:Basic ${JSON.stringify(decoded.slice(0, decoded.indexOf(':')))}:${
|
|
755
|
+
return /^[\x20-\x7e]*:/.test(decoded) ? `${name}:Basic ${JSON.stringify(decoded.slice(0, decoded.indexOf(':')))}:${secretFor(decoded.slice(decoded.indexOf(':') + 1))}` : `${name}:${secretFor(value)}`;
|
|
724
756
|
}
|
|
725
757
|
const redacted = redactText(header, state);
|
|
726
758
|
return redacted === header ? withheld(state, header) : redacted;
|
|
@@ -739,9 +771,9 @@ function redactSegment(words, ctx) {
|
|
|
739
771
|
return `${name}=${cutCredential(value, state)}`;
|
|
740
772
|
};
|
|
741
773
|
// The exact pass: `curl -u USER:PASSWORD` names who acts; only the password is certain.
|
|
742
|
-
const exactUser = (value) => (value.includes(':') ? `${value.slice(0, value.indexOf(':'))}:${
|
|
774
|
+
const exactUser = (value) => (value.includes(':') ? `${value.slice(0, value.indexOf(':'))}:${secretFor(value.slice(value.indexOf(':') + 1))}` : value);
|
|
743
775
|
// The exact pass: openssl `pass:X` and a bare value are the password; `env:`, `file:`, `fd:` name where it is.
|
|
744
|
-
const exactOpensslPass = (value) => (/^(?:env|file|fd):/.test(value) ? value : /^pass:/.test(value) ? `pass:${
|
|
776
|
+
const exactOpensslPass = (value) => (/^(?:env|file|fd):/.test(value) ? value : /^pass:/.test(value) ? `pass:${secretFor(value.slice(5))}` : secretFor(value));
|
|
745
777
|
// A program named for a secret (set-password.sh): its arguments cannot be told apart (the exact pass keeps them).
|
|
746
778
|
if (!SECRET_PROGRAMS.has(stem) && classifySecretName(stem) === 'credential') {
|
|
747
779
|
for (const i of positionals)
|
|
@@ -771,7 +803,7 @@ function redactSegment(words, ctx) {
|
|
|
771
803
|
};
|
|
772
804
|
const attachedOrNext = (prefix) => {
|
|
773
805
|
if (arg.length > prefix.length) {
|
|
774
|
-
out[i] = `${prefix}${
|
|
806
|
+
out[i] = `${prefix}${secretFor(arg.slice(prefix.length))}`;
|
|
775
807
|
handled.add(i);
|
|
776
808
|
}
|
|
777
809
|
else {
|
|
@@ -790,12 +822,12 @@ function redactSegment(words, ctx) {
|
|
|
790
822
|
if (long) {
|
|
791
823
|
const name = long[1].replace(/^-+/, '');
|
|
792
824
|
if (name === 'from-literal' || name === 'from-env-literal') {
|
|
793
|
-
const replaced = `${long[1]}=${long[2].replace(/^([^=]*)
|
|
825
|
+
const replaced = `${long[1]}=${long[2].replace(/^([^=]*)=(.*)$/s, (_m, literal, secret) => `${literal}=${secretFor(secret)}`)}`;
|
|
794
826
|
out[i] = holdsMoreThanCredential(long[2].replace(/^[^=]*=/, '')) ? withheld(state, arg, replaced) : replaced;
|
|
795
827
|
handled.add(i);
|
|
796
828
|
}
|
|
797
829
|
else if (CURL_FAMILY.has(program) && /^(?:u|user|proxy-user)$/.test(name)) {
|
|
798
|
-
out[i] = `${long[1]}=${state.exact ? exactUser(long[2]) :
|
|
830
|
+
out[i] = `${long[1]}=${state.exact ? exactUser(long[2]) : secretFor(long[2])}`;
|
|
799
831
|
handled.add(i);
|
|
800
832
|
}
|
|
801
833
|
else if (CURL_FAMILY.has(program) && name === 'header' && AUTH_HEADER.test(long[2])) {
|
|
@@ -846,7 +878,7 @@ function redactSegment(words, ctx) {
|
|
|
846
878
|
// Short flags by program; single-dash long names (-password, -token) by name.
|
|
847
879
|
if (/^-[A-Za-z]/.test(arg)) {
|
|
848
880
|
if (MYSQL_FAMILY.has(program) && /^-p.+/.test(arg)) {
|
|
849
|
-
out[i] = `-p${
|
|
881
|
+
out[i] = `-p${secretFor(arg.slice(2))}`;
|
|
850
882
|
handled.add(i);
|
|
851
883
|
}
|
|
852
884
|
else if (ALWAYS_P.has(program) && /^-p/.test(arg))
|
|
@@ -889,7 +921,7 @@ function redactSegment(words, ctx) {
|
|
|
889
921
|
const [, name, value] = assignment;
|
|
890
922
|
if (secretContext) {
|
|
891
923
|
// In a secrets command every value is a secret: withheld whole, and truncated if it held more fields.
|
|
892
|
-
out[i] = /[&#]/.test(value) || holdsMoreThanCredential(value) ? withheld(state, arg, `${name}=${
|
|
924
|
+
out[i] = /[&#]/.test(value) || holdsMoreThanCredential(value) ? withheld(state, arg, `${name}=${secretFor(value)}`) : `${name}=${secretFor(value)}`;
|
|
893
925
|
handled.add(i);
|
|
894
926
|
}
|
|
895
927
|
else if (holdsSecret(name)) {
|
|
@@ -909,7 +941,7 @@ function redactSegment(words, ctx) {
|
|
|
909
941
|
continue;
|
|
910
942
|
}
|
|
911
943
|
if (program === 'openssl' && /^(?:pass|env|file|fd):/.test(arg)) {
|
|
912
|
-
out[i] = state.exact ? exactOpensslPass(arg) : arg.replace(
|
|
944
|
+
out[i] = state.exact ? exactOpensslPass(arg) : arg.replace(/:(.*)$/s, (_m, rest) => `:${secretFor(rest)}`);
|
|
913
945
|
handled.add(i);
|
|
914
946
|
continue;
|
|
915
947
|
}
|
|
@@ -1264,7 +1296,7 @@ function redactedInput(value, state, depth = 0) {
|
|
|
1264
1296
|
if (state.exact)
|
|
1265
1297
|
throw new NormalizationLimit();
|
|
1266
1298
|
state.truncated = true; // exact: raw (canonical input)
|
|
1267
|
-
return
|
|
1299
|
+
return secretOfValue(value);
|
|
1268
1300
|
}
|
|
1269
1301
|
if (typeof value === 'string')
|
|
1270
1302
|
return redactText(value, state);
|
|
@@ -1289,7 +1321,7 @@ function redactedInput(value, state, depth = 0) {
|
|
|
1289
1321
|
// name (`credentials: {user, role, password}`): only a value itself is certain.
|
|
1290
1322
|
if (kind === 'ambiguous')
|
|
1291
1323
|
state.truncated = true; // exact: kept (below)
|
|
1292
|
-
out[key] = state.exact && (kind === 'ambiguous' || (item !== null && typeof item === 'object')) ? redactedInput(item, state, depth + 1) :
|
|
1324
|
+
out[key] = state.exact && (kind === 'ambiguous' || (item !== null && typeof item === 'object')) ? redactedInput(item, state, depth + 1) : secretOfValue(item);
|
|
1293
1325
|
}
|
|
1294
1326
|
else {
|
|
1295
1327
|
out[key] = redactedInput(item, state, depth + 1);
|
|
@@ -1456,7 +1488,7 @@ function inputPreimage(input, state) {
|
|
|
1456
1488
|
if (input === undefined || input === null)
|
|
1457
1489
|
return null;
|
|
1458
1490
|
const text = JSON.stringify(sortedValue(redactedInput(input, state)));
|
|
1459
|
-
if (text
|
|
1491
|
+
if (hasSecretMarker(text))
|
|
1460
1492
|
state.truncated = true; // exact: n/a (pass 1 only; exactMaterial is the exact form)
|
|
1461
1493
|
return `${HASH_VERSION}\ninput\n${text}`;
|
|
1462
1494
|
}
|
|
@@ -1800,7 +1832,7 @@ function normalizedOfKind(event, kind, hostToolName, state) {
|
|
|
1800
1832
|
state.freeText = !(0, hook_tool_policy_1.isShellTextTool)(event);
|
|
1801
1833
|
const { text, programs } = normalizeInner(raw, state, 0);
|
|
1802
1834
|
// A secret was replaced: the action is never bound or reused (it asks each time).
|
|
1803
|
-
if (text
|
|
1835
|
+
if (hasSecretMarker(text))
|
|
1804
1836
|
state.truncated = true; // exact: n/a (pass 1 only; the exact pass decides each value)
|
|
1805
1837
|
// The command text itself only for a deploy the service can judge word for word (see deployCommandText).
|
|
1806
1838
|
// Judged on the host's exact text (a trimmed non-ASCII space must not slip through).
|
|
@@ -1841,7 +1873,8 @@ function normalizedOfKind(event, kind, hostToolName, state) {
|
|
|
1841
1873
|
if (targets.length > 64)
|
|
1842
1874
|
state.truncated = true; // exact: n/a (paths are not exact material; the whole input is)
|
|
1843
1875
|
const paths = targets.slice(0, 64).map((path) => {
|
|
1844
|
-
|
|
1876
|
+
// The paths go to the service's path classifier as they did before the key class: class-free (the class is in the hashes).
|
|
1877
|
+
const redacted = redactPathSpans(redactText(withoutUserName(path, state), state)).replace(SECRET_MARKER_ALL, SECRET);
|
|
1845
1878
|
if (redacted.length > 512 || redacted.includes(SECRET) || path.length > PATH_SCAN_CHARS)
|
|
1846
1879
|
state.truncated = true; // exact: n/a (as above)
|
|
1847
1880
|
return redacted.slice(0, 512);
|