@sema-agent/core 7.33.1 → 7.33.2
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/CHANGELOG.md +10 -0
- package/dist/core/secret-env.d.ts +2 -2
- package/dist/core/secret-env.js +62 -7
- package/package.json +1 -1
- package/wire-changes/core@7.33.2.json +59 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.33.2 — 2026-09-26
|
|
4
|
+
|
|
5
|
+
### Security fix — git's config VALUES are judged by their value: `GIT_CONFIG_VALUE_<i>` carrying an `Authorization:` header or a userinfo URL is dropped (#1129 follow-up; behavior; `SecretEnvFindingKind` +`family-rule`; +0 exports, +0 wire keys) (@server)
|
|
6
|
+
|
|
7
|
+
- **Before.** 7.33.1 exempted `GIT_CONFIG_KEY_<i>` from the shape rule with a value screen on the KEY (URL userinfo). The VALUE member has no credential word in its name, so the scrub never judged it — `GIT_CONFIG_COUNT=1 / GIT_CONFIG_KEY_0=http.extraHeader / GIT_CONFIG_VALUE_0="Authorization: Bearer <token>"` passed whole. A host that withholds the group only when core withholds a member (the server's all-or-nothing rule) therefore handed the token to the task's shell; 7.32.1 had withheld the whole group because the KEY fell to the shape rule. A regression relative to the shipped chain (server 7.102.0), found by the server's codex back-review.
|
|
8
|
+
- **Now.** The credential side of the exact-name arm reads a named family too: `GIT_CONFIG_VALUE_<i>` whose value is an HTTP credential header (`Authorization:` / `Proxy-Authorization:` / `Cookie:`, any case, leading whitespace allowed) or a URL with a userinfo (`<scheme>:/+<user>[:<secret>]@…`) is a credential and drops, reported as `kind: "family-rule"` (confidence high — the value was read). The KEY screen reads a slash run (`https:///u:t@host`, which curl accepts, dropped like `https://`). A KEY whose value fails its screen is now reported as `family-rule` too (it was `suffix-rule`; KL-1129d closed). The screen reads the authority as git and curl do — it ends at the first `/`, `?` or `#`, so an `@` in a query or fragment is not a userinfo — and an `Authorization:` header with nothing after the colon (curl's spelling for "send no such header") is not a credential: both stay and their group runs; a header on the value's SECOND line (git hands `http.extraHeader` to libcurl verbatim, a line break carries another header — real git 2.54 / libcurl 8.7.1 put it on the wire) is read like the first. A proxy VALUE with a userinfo and no scheme (`u:<secret>@host:port` — libcurl assumes http, git prompts for a missing password) drops when its paired KEY (`GIT_CONFIG_KEY_<i>`, same index) names a proxy (`http.proxy` / `http.<url>.proxy` / `remote.<name>.proxy`): the screen reads the group, which `scrubSecretEnv` always has; `isSecretEnvKey(key, value?, env?)` gains an optional third parameter for callers that classify one entry (without it the proxy screen is silent — `a@b.example` is also an e-mail address). A folded header (`Authorization:` + a continuation line beginning with SP / HTAB) is a header with content; a Unicode line separator is not a line break to HTTP and does not split headers. On a proxy key the userinfo may hold any byte but `/` and `@` (curl accepts U+00A0 in a password), the scheme is optional with any slash count (`https:/…` folds), and the key's subsection may hold any character git accepts. Each screen reads as its consumer does: git-URL (`://` or more slashes — a single slash is git's scp-like `host:path`, so `example.invalid:/repo@v2.git` and `git@github.com:org/repo.git` stay, `ssh://git@github.com/…` drops as a URL userinfo), curl-header, curl-proxy. A family stem is asserted `[A-Z0-9_]+` at load (KL-1129c closed). The URL screen is anchored on the `:` and reads the scheme backward, linear in the value's length (the scheme-first form was quadratic: ~1 s for an 8 KiB header-shaped value, on the host's event loop at every spawn — a codex falsification round measured it). No value-shape screen outside the git family: a header-looking value on any other name is untouched (#1134 records the policy question).
|
|
9
|
+
- **Who needs it.** Deployments whose host passes git config through the environment and relies on the group rule to keep an `http.extraHeader` token out of tool subprocesses.
|
|
10
|
+
- **Who is hurt / compensation.** A git group whose VALUE is a URL with a user name and no secret (`remote.<name>.url = ssh://git@github.com/org/repo.git`, `https://jane@host/…`) is treated like the KEY side already was in 7.33.1 (KL-1129b: a bare user can itself be a token) and breaks — write such remotes in the repository's config or in scp form (`git@github.com:org/repo.git` stays). A git group whose VALUE carries a credential no longer reaches the child (git exits 128 `missing config value` on the broken group — fail-closed; the host's group rule withholds the rest); the `SecretEnvFindingKind` word set gains `family-rule` (metrics labels pass it through; exhaustive switches over the four words must add an arm). Compensation: credentials to git children go through a credential helper, not the environment.
|
|
11
|
+
- Pins: `test/backlog1129-git-config-key-family.test.ts` (7.33.2 block: header spellings, userinfo URLs, path-`@` and plain values kept, slash run, family verdict kinds; ② block re-pinned to `family-rule`).
|
|
12
|
+
|
|
3
13
|
## 7.33.1 — 2026-09-26
|
|
4
14
|
|
|
5
15
|
### The first-byte lane's retry is a construction-time seat — `BrainTimeoutConfig.firstByteRetries?: 0 | 1` (#1128; additive; +1 optional type key; +0 exports, +0 codes) (@server)
|
|
@@ -25,7 +25,7 @@ export interface EnvNameTable {
|
|
|
25
25
|
has(upperName: string, value?: string): boolean;
|
|
26
26
|
}
|
|
27
27
|
/** Which rule classified a key as secret-shaped — the env arm's `kind` vocabulary. */
|
|
28
|
-
export type SecretEnvFindingKind = "suffix-rule" | "namespace-rule" | "exact-name";
|
|
28
|
+
export type SecretEnvFindingKind = "suffix-rule" | "namespace-rule" | "exact-name" | "family-rule";
|
|
29
29
|
/** One env entry the `scrub` policy dropped: the key (the VALUE is never carried — this record is meant
|
|
30
30
|
* to be loggable), the rule that classified it, how strong that claim is, and the minting module. */
|
|
31
31
|
export interface SecretEnvFinding {
|
|
@@ -46,7 +46,7 @@ export declare function classifyExactEnvName(key: string, credentialNames: EnvNa
|
|
|
46
46
|
/** Is `key` a secret-pattern env var (one the fail-closed `scrub` policy drops before spawning a shell)? Pass the
|
|
47
47
|
* entry's `value` to get exactly the scrub's answer for that entry: a named-family exemption holds only for a value
|
|
48
48
|
* that clears the family's screen, so without a value such a member reads as secret (the name alone cannot clear). */
|
|
49
|
-
export declare function isSecretEnvKey(key: string, value?: string): boolean;
|
|
49
|
+
export declare function isSecretEnvKey(key: string, value?: string, env?: NodeJS.ProcessEnv): boolean;
|
|
50
50
|
/** Drop every secret-pattern key (see {@link isSecretEnvKey}) from an env object — the fail-closed `scrub`
|
|
51
51
|
* policy. `findings` is an OPTIONAL caller-owned collector: pass an array to learn which entries were
|
|
52
52
|
* dropped and why, read it back after the call. Omitting it leaves this function byte-for-byte what it
|
package/dist/core/secret-env.js
CHANGED
|
@@ -15,11 +15,62 @@ const NON_SECRET_ENV_EXACT_NAMES = new Set([
|
|
|
15
15
|
"MAX_BATCH_PREFILL_TOKENS", "MAX_WAITING_TOKENS", "MAX_TOP_N_TOKENS", "RATE_LIMIT_TOKENS",
|
|
16
16
|
"PROMPT_TOKENS", "CONTEXT_TOKENS",
|
|
17
17
|
]);
|
|
18
|
+
const URL_USERINFO_RE = /:(?<=[a-z][a-z0-9+.-]*:)\/{2,}[^/?#@]+@/i;
|
|
19
|
+
const HEADER_LINE_BREAK_RE = /\r\n|\r|\n/;
|
|
20
|
+
const AUTHORIZATION_HEADER_NAME_RE = /^[ \t]*(?:(?:proxy-)?authorization|cookie)[ \t]*:/i;
|
|
21
|
+
const HEADER_CONTENT_RE = /[^ \t\f\v]/;
|
|
22
|
+
function carriesAuthorizationHeader(value) {
|
|
23
|
+
let logical;
|
|
24
|
+
const judge = () => logical !== undefined && AUTHORIZATION_HEADER_NAME_RE.test(logical) && HEADER_CONTENT_RE.test(logical.slice(logical.indexOf(":") + 1));
|
|
25
|
+
for (const physical of value.split(HEADER_LINE_BREAK_RE)) {
|
|
26
|
+
if (logical !== undefined && (physical.startsWith(" ") || physical.startsWith("\t"))) {
|
|
27
|
+
logical += " " + physical;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (judge())
|
|
31
|
+
return true;
|
|
32
|
+
logical = physical;
|
|
33
|
+
}
|
|
34
|
+
return judge();
|
|
35
|
+
}
|
|
36
|
+
const GIT_PROXY_CONFIG_KEY_RE = /^(?:http(?:\..+)?|remote\..+)\.proxy$/is;
|
|
37
|
+
const PROXY_AUTHORITY_USERINFO_RE = /^(?:[a-z][a-z0-9+.-]*:\/*)?[^/?#@]+@/i;
|
|
18
38
|
const NON_SECRET_ENV_INDEXED_FAMILIES = [
|
|
19
|
-
{ stem: "GIT_CONFIG_KEY", valueClears: (value) =>
|
|
39
|
+
{ stem: "GIT_CONFIG_KEY", valueClears: (value) => !URL_USERINFO_RE.test(value) },
|
|
40
|
+
];
|
|
41
|
+
const SECRET_ENV_INDEXED_FAMILIES = [
|
|
42
|
+
{
|
|
43
|
+
stem: "GIT_CONFIG_VALUE",
|
|
44
|
+
credentialWhen: (value, group) => {
|
|
45
|
+
if (carriesAuthorizationHeader(value) || URL_USERINFO_RE.test(value))
|
|
46
|
+
return true;
|
|
47
|
+
const key = group.sibling("GIT_CONFIG_KEY");
|
|
48
|
+
return key !== undefined && GIT_PROXY_CONFIG_KEY_RE.test(key) && PROXY_AUTHORITY_USERINFO_RE.test(value);
|
|
49
|
+
},
|
|
50
|
+
},
|
|
20
51
|
];
|
|
21
|
-
const indexedFamilyMember = (stem) =>
|
|
52
|
+
const indexedFamilyMember = (stem) => {
|
|
53
|
+
if (!/^[A-Z0-9_]+$/.test(stem))
|
|
54
|
+
throw new Error(`secret-env: indexed family stem ${JSON.stringify(stem)} must match [A-Z0-9_]+`);
|
|
55
|
+
return new RegExp(`^${stem}_(0|[1-9][0-9]*)$`);
|
|
56
|
+
};
|
|
22
57
|
const NON_SECRET_ENV_INDEXED_FAMILY_ROWS = NON_SECRET_ENV_INDEXED_FAMILIES.map((f) => ({ member: indexedFamilyMember(f.stem), valueClears: f.valueClears }));
|
|
58
|
+
const SECRET_ENV_INDEXED_FAMILY_ROWS = SECRET_ENV_INDEXED_FAMILIES.map((f) => ({ member: indexedFamilyMember(f.stem), credentialWhen: f.credentialWhen }));
|
|
59
|
+
function classifyByFamily(upperName, value, env) {
|
|
60
|
+
if (value === undefined)
|
|
61
|
+
return undefined;
|
|
62
|
+
for (const row of SECRET_ENV_INDEXED_FAMILY_ROWS) {
|
|
63
|
+
const member = row.member.exec(upperName);
|
|
64
|
+
if (member === null)
|
|
65
|
+
continue;
|
|
66
|
+
const index = member[1];
|
|
67
|
+
if (row.credentialWhen(value, { sibling: (stem) => env?.[`${stem}_${index}`] }))
|
|
68
|
+
return "credential";
|
|
69
|
+
}
|
|
70
|
+
if (NON_SECRET_ENV_INDEXED_FAMILY_ROWS.some((row) => row.member.test(upperName) && !row.valueClears(value)))
|
|
71
|
+
return "screen-failed";
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
23
74
|
const NON_SECRET_ENV_NAMES = {
|
|
24
75
|
has: (upperName, value) => NON_SECRET_ENV_EXACT_NAMES.has(upperName) ||
|
|
25
76
|
(value !== undefined && NON_SECRET_ENV_INDEXED_FAMILY_ROWS.some((row) => row.member.test(upperName) && row.valueClears(value))),
|
|
@@ -28,6 +79,7 @@ const SECRET_ENV_KIND_CONFIDENCE = {
|
|
|
28
79
|
"exact-name": "high",
|
|
29
80
|
"suffix-rule": "medium",
|
|
30
81
|
"namespace-rule": "medium",
|
|
82
|
+
"family-rule": "high",
|
|
31
83
|
};
|
|
32
84
|
export function classifyExactEnvName(key, credentialNames, exemptNames, value) {
|
|
33
85
|
const upper = key.toUpperCase();
|
|
@@ -37,23 +89,26 @@ export function classifyExactEnvName(key, credentialNames, exemptNames, value) {
|
|
|
37
89
|
return "exempt";
|
|
38
90
|
return "unknown";
|
|
39
91
|
}
|
|
40
|
-
function classifySecretEnvKey(key, value) {
|
|
92
|
+
function classifySecretEnvKey(key, value, env) {
|
|
41
93
|
const byExactName = classifyExactEnvName(key, SECRET_ENV_EXACT_NAMES, NON_SECRET_ENV_NAMES, value);
|
|
42
94
|
if (byExactName === "credential")
|
|
43
95
|
return "exact-name";
|
|
44
96
|
if (byExactName === "exempt")
|
|
45
97
|
return undefined;
|
|
98
|
+
const byFamily = classifyByFamily(key.toUpperCase(), value, env);
|
|
99
|
+
if (byFamily === "credential")
|
|
100
|
+
return "family-rule";
|
|
46
101
|
if (SECRET_ENV_RE.test(key))
|
|
47
|
-
return "suffix-rule";
|
|
102
|
+
return byFamily === "screen-failed" ? "family-rule" : "suffix-rule";
|
|
48
103
|
return SEMA_NAMESPACE_SECRET_RE.test(key) ? "namespace-rule" : undefined;
|
|
49
104
|
}
|
|
50
|
-
export function isSecretEnvKey(key, value) {
|
|
51
|
-
return classifySecretEnvKey(key, value) !== undefined;
|
|
105
|
+
export function isSecretEnvKey(key, value, env) {
|
|
106
|
+
return classifySecretEnvKey(key, value, env) !== undefined;
|
|
52
107
|
}
|
|
53
108
|
export function scrubSecretEnv(env, findings) {
|
|
54
109
|
const out = {};
|
|
55
110
|
for (const k of Object.keys(env)) {
|
|
56
|
-
const kind = classifySecretEnvKey(k, env[k]);
|
|
111
|
+
const kind = classifySecretEnvKey(k, env[k], env);
|
|
57
112
|
if (kind === undefined) {
|
|
58
113
|
out[k] = env[k];
|
|
59
114
|
continue;
|
package/package.json
CHANGED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"repo": "core",
|
|
3
|
+
"version": "7.33.2",
|
|
4
|
+
"from": "869ac44b1f76fcf61f658f1652370b74b500868c",
|
|
5
|
+
"to": "v7.33.2",
|
|
6
|
+
"added": [],
|
|
7
|
+
"removed": [],
|
|
8
|
+
"changed": [],
|
|
9
|
+
"narrowed": [],
|
|
10
|
+
"closedSetMembers": [
|
|
11
|
+
{
|
|
12
|
+
"set": "SecretEnvFindingKind",
|
|
13
|
+
"at": [
|
|
14
|
+
"src/core/secret-env.ts:329"
|
|
15
|
+
],
|
|
16
|
+
"newSet": false,
|
|
17
|
+
"added": [
|
|
18
|
+
"family-rule"
|
|
19
|
+
],
|
|
20
|
+
"removed": [],
|
|
21
|
+
"breaking": false,
|
|
22
|
+
"criteriaCovered": false
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"closedSetsUnreadable": [],
|
|
26
|
+
"typeKeys": [],
|
|
27
|
+
"typeKeysUnreadableArms": [],
|
|
28
|
+
"typeKeysFlattenedCount": 217,
|
|
29
|
+
"declarationMoves": [],
|
|
30
|
+
"mintMoves": [],
|
|
31
|
+
"criteriaObserves": [
|
|
32
|
+
{
|
|
33
|
+
"kind": "typeMember",
|
|
34
|
+
"name": "SecretEnvFinding.key",
|
|
35
|
+
"assertions": [
|
|
36
|
+
"G1.a"
|
|
37
|
+
]
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"kind": "typeMember",
|
|
41
|
+
"name": "SecretEnvFinding.kind",
|
|
42
|
+
"assertions": [
|
|
43
|
+
"G1.a",
|
|
44
|
+
"G1.b"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"consumersHint": {
|
|
49
|
+
"cli": [],
|
|
50
|
+
"server": [
|
|
51
|
+
"SecretEnvFindingKind"
|
|
52
|
+
],
|
|
53
|
+
"sdk": [],
|
|
54
|
+
"note": "scanned cli@4e593722 server@a4cd5728 sdk@4031f42 — a name listed is MENTIONED there today, which is evidence of consumption, not proof of it"
|
|
55
|
+
},
|
|
56
|
+
"breakingCount": 0,
|
|
57
|
+
"breakingItems": [],
|
|
58
|
+
"toResolvedAt": "3eae7e3fe99b608206dc0f9d0b9fb257f012d0f1"
|
|
59
|
+
}
|