instar 1.3.1663 → 1.3.1664
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/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +9 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/SafeGitExecutor.d.ts +33 -11
- package/dist/core/SafeGitExecutor.d.ts.map +1 -1
- package/dist/core/SafeGitExecutor.js +144 -116
- package/dist/core/SafeGitExecutor.js.map +1 -1
- package/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/dist/native/InstarWedgeCanary.app/Contents/MacOS/InstarWedgeCanary +0 -0
- package/dist/native/instar-desktop-host +0 -0
- package/dist/recommended/catalog.js +1 -1
- package/dist/recommended/catalog.js.map +1 -1
- package/dist/recommended/recipes.d.ts.map +1 -1
- package/dist/recommended/recipes.js +3 -2
- package/dist/recommended/recipes.js.map +1 -1
- package/dist/recommended/setupState.d.ts +9 -0
- package/dist/recommended/setupState.d.ts.map +1 -1
- package/dist/recommended/setupState.js +17 -0
- package/dist/recommended/setupState.js.map +1 -1
- package/dist/server/AgentServer.d.ts.map +1 -1
- package/dist/server/AgentServer.js +5 -0
- package/dist/server/AgentServer.js.map +1 -1
- package/dist/server/recommendedServiceRoutes.d.ts +2 -0
- package/dist/server/recommendedServiceRoutes.d.ts.map +1 -1
- package/dist/server/recommendedServiceRoutes.js +41 -7
- package/dist/server/recommendedServiceRoutes.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +7 -0
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +47 -47
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1664.md +32 -0
- package/upgrades/side-effects/recommended-services-stop-holds.md +64 -0
- package/upgrades/side-effects/safe-git-one-verb-classifier.md +117 -0
|
@@ -34,11 +34,19 @@
|
|
|
34
34
|
import { type ChildProcess } from 'node:child_process';
|
|
35
35
|
import { SourceTreeGuardError } from './SourceTreeGuard.js';
|
|
36
36
|
/**
|
|
37
|
-
*
|
|
37
|
+
* How the funnel classes a verb.
|
|
38
|
+
* read — every form is treated as a read.
|
|
39
|
+
* write — every form is treated as a write.
|
|
40
|
+
* shape — the verb has both forms; isReadOnlyShape decides per call.
|
|
38
41
|
*/
|
|
42
|
+
export type GitVerbClass = 'read' | 'write' | 'shape';
|
|
43
|
+
export declare const GIT_VERB_CLASSES: ReadonlyMap<string, GitVerbClass>;
|
|
44
|
+
/** Verbs a write surface can run: `write` and `shape`. Derived from the table. */
|
|
39
45
|
export declare const DESTRUCTIVE_GIT_VERBS: ReadonlySet<string>;
|
|
40
46
|
/**
|
|
41
|
-
*
|
|
47
|
+
* Verbs a read surface can run: `read` and `shape`. Derived from the table.
|
|
48
|
+
* Membership is the funnel's classification, NOT proof the verb writes nothing:
|
|
49
|
+
* some forms of these verbs can write or run a program (EVO-029 items 4 and 5).
|
|
42
50
|
*/
|
|
43
51
|
export declare const READONLY_GIT_VERBS: ReadonlySet<string>;
|
|
44
52
|
/**
|
|
@@ -81,6 +89,13 @@ declare function extractVerbAndTargets(args: readonly string[], cwd: string | un
|
|
|
81
89
|
* membership).
|
|
82
90
|
*/
|
|
83
91
|
declare function isReadOnlyShape(verb: string, verbArgs: readonly string[]): boolean | null;
|
|
92
|
+
export type GitInvocationClass = 'read' | 'write' | 'unclassified';
|
|
93
|
+
/**
|
|
94
|
+
* The one answer to "what does the funnel class this invocation as". `read`
|
|
95
|
+
* means the funnel classifies it as a read, not that git will write nothing.
|
|
96
|
+
* A verb outside GIT_VERB_CLASSES is `unclassified`, never read or write.
|
|
97
|
+
*/
|
|
98
|
+
export declare function classifyGitInvocation(verb: string, verbArgs: readonly string[]): GitInvocationClass;
|
|
84
99
|
interface AuditEntry {
|
|
85
100
|
timestamp: string;
|
|
86
101
|
executor: 'git' | 'fs';
|
|
@@ -234,13 +249,15 @@ export declare class SafeGitExecutor {
|
|
|
234
249
|
* Synchronous destructive git execution.
|
|
235
250
|
*
|
|
236
251
|
* 1. Extract the verb and ALL target directories from args + opts.cwd.
|
|
237
|
-
* 2.
|
|
238
|
-
* 3.
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
* 5.
|
|
252
|
+
* 2. Refuse an unclassified verb, before anything else, on every surface.
|
|
253
|
+
* 3. Canonicalize each target and call assertNotInstarSourceTree on each.
|
|
254
|
+
* 4. Verify the invocation classifies as a write. Reads throw — callers must
|
|
255
|
+
* use readSync.
|
|
256
|
+
* 5. Strip env denylist; inject GIT_CONFIG_GLOBAL/SYSTEM=/dev/null.
|
|
257
|
+
* 6. Spawn git via execFileSync; return stdout.
|
|
242
258
|
*
|
|
243
|
-
* Throws SourceTreeGuardError if any target is the instar source tree
|
|
259
|
+
* Throws SourceTreeGuardError if any target is the instar source tree and the
|
|
260
|
+
* verb is classified.
|
|
244
261
|
* Throws SafeGitExecutorError on classification mismatch or arg errors.
|
|
245
262
|
*/
|
|
246
263
|
static execSync(args: readonly string[], opts: SafeGitOptions): string;
|
|
@@ -262,9 +279,13 @@ export declare class SafeGitExecutor {
|
|
|
262
279
|
*/
|
|
263
280
|
static readSync(args: readonly string[], opts: SafeGitOptions): string;
|
|
264
281
|
/**
|
|
265
|
-
* Verb-aware dispatcher.
|
|
266
|
-
*
|
|
267
|
-
* args and don't know at the
|
|
282
|
+
* Verb-aware dispatcher. A write goes to execSync and everything else to
|
|
283
|
+
* readSync, which extracts the verb itself and makes the real decision. Use
|
|
284
|
+
* this from generic git() helpers that take dynamic args and don't know at the
|
|
285
|
+
* call site whether the verb is destructive.
|
|
286
|
+
*
|
|
287
|
+
* It reads args[0] as the verb, so a pre-verb flag such as `-c` classifies as
|
|
288
|
+
* unclassified here and lands on readSync (EVO-029 item 1).
|
|
268
289
|
*/
|
|
269
290
|
static run(args: readonly string[], opts: SafeGitOptions): string;
|
|
270
291
|
}
|
|
@@ -284,6 +305,7 @@ export declare function isLocalVerifyClonePath(target: string, verifyRoot: strin
|
|
|
284
305
|
export declare const _internal: {
|
|
285
306
|
extractVerbAndTargets: typeof extractVerbAndTargets;
|
|
286
307
|
isReadOnlyShape: typeof isReadOnlyShape;
|
|
308
|
+
GIT_VERB_CLASS_ENTRIES: readonly (readonly [string, GitVerbClass])[];
|
|
287
309
|
sanitizeEnv: typeof sanitizeEnv;
|
|
288
310
|
GIT_ENV_DENYLIST: ReadonlySet<string>;
|
|
289
311
|
repoHasLocalIdentity: typeof repoHasLocalIdentity;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SafeGitExecutor.d.ts","sourceRoot":"","sources":["../../src/core/SafeGitExecutor.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,OAAO,EAGL,KAAK,YAAY,EAElB,MAAM,oBAAoB,CAAC;AAM5B,OAAO,EAEL,oBAAoB,EACrB,MAAM,sBAAsB,CAAC;AAI9B
|
|
1
|
+
{"version":3,"file":"SafeGitExecutor.d.ts","sourceRoot":"","sources":["../../src/core/SafeGitExecutor.ts"],"names":[],"mappings":"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"}
|
|
@@ -39,92 +39,96 @@ import path from 'node:path';
|
|
|
39
39
|
import { fileURLToPath } from 'node:url';
|
|
40
40
|
import { withSyncOp } from './InFlightSyncOpMarker.js';
|
|
41
41
|
import { assertNotInstarSourceTree, SourceTreeGuardError, } from './SourceTreeGuard.js';
|
|
42
|
-
// ── Verb classification ─────────────────────────────────────────────
|
|
43
42
|
/**
|
|
44
|
-
*
|
|
43
|
+
* The one verb classification every surface reads. Closed enumeration; an
|
|
44
|
+
* addition is a spec change. A verb that is not listed is unclassified, and no
|
|
45
|
+
* surface will run it. Kept as raw entries so a test can see a duplicate, which
|
|
46
|
+
* the Map built from them would hide.
|
|
45
47
|
*/
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'checkout',
|
|
52
|
-
'cherry-pick',
|
|
53
|
-
'clean',
|
|
54
|
-
'clone',
|
|
55
|
-
'commit',
|
|
56
|
-
'fetch',
|
|
57
|
-
'gc',
|
|
58
|
-
'init',
|
|
59
|
-
'merge',
|
|
60
|
-
'mv',
|
|
61
|
-
'pull',
|
|
62
|
-
'push',
|
|
63
|
-
'rebase',
|
|
64
|
-
'reset',
|
|
65
|
-
'restore',
|
|
66
|
-
'revert',
|
|
67
|
-
'rm',
|
|
68
|
-
'
|
|
69
|
-
'
|
|
70
|
-
'
|
|
71
|
-
'
|
|
72
|
-
'update-ref',
|
|
48
|
+
const GIT_VERB_CLASS_ENTRIES = [
|
|
49
|
+
// Writes.
|
|
50
|
+
['add', 'write'],
|
|
51
|
+
['am', 'write'],
|
|
52
|
+
['apply', 'write'],
|
|
53
|
+
['checkout', 'write'],
|
|
54
|
+
['cherry-pick', 'write'],
|
|
55
|
+
['clean', 'write'],
|
|
56
|
+
['clone', 'write'],
|
|
57
|
+
['commit', 'write'],
|
|
58
|
+
['fetch', 'write'],
|
|
59
|
+
['gc', 'write'],
|
|
60
|
+
['init', 'write'],
|
|
61
|
+
['merge', 'write'],
|
|
62
|
+
['mv', 'write'],
|
|
63
|
+
['pull', 'write'],
|
|
64
|
+
['push', 'write'],
|
|
65
|
+
['rebase', 'write'],
|
|
66
|
+
['reset', 'write'],
|
|
67
|
+
['restore', 'write'],
|
|
68
|
+
['revert', 'write'],
|
|
69
|
+
['rm', 'write'],
|
|
70
|
+
['submodule', 'write'],
|
|
71
|
+
['switch', 'write'],
|
|
72
|
+
['tag', 'write'],
|
|
73
|
+
['update-ref', 'write'],
|
|
73
74
|
// Object-database writers (no ref or working-tree change); destructive-classified so
|
|
74
75
|
// they route through execSync. local-verify's merge-tree/commit-tree path (§2 step 1).
|
|
75
|
-
'merge-tree',
|
|
76
|
-
'commit-tree',
|
|
77
|
-
'
|
|
78
|
-
'
|
|
79
|
-
'
|
|
80
|
-
'
|
|
81
|
-
|
|
82
|
-
'
|
|
83
|
-
'
|
|
84
|
-
'
|
|
85
|
-
]
|
|
76
|
+
['merge-tree', 'write'],
|
|
77
|
+
['commit-tree', 'write'],
|
|
78
|
+
['prune', 'write'],
|
|
79
|
+
['notes', 'write'],
|
|
80
|
+
['replace', 'write'],
|
|
81
|
+
['filter-branch', 'write'],
|
|
82
|
+
// Reads.
|
|
83
|
+
['status', 'read'],
|
|
84
|
+
['log', 'read'],
|
|
85
|
+
['diff', 'read'],
|
|
86
|
+
['show', 'read'],
|
|
87
|
+
['rev-parse', 'read'],
|
|
88
|
+
['rev-list', 'read'],
|
|
89
|
+
['ls-files', 'read'],
|
|
90
|
+
['ls-tree', 'read'],
|
|
91
|
+
['ls-remote', 'read'],
|
|
92
|
+
['describe', 'read'],
|
|
93
|
+
['name-rev', 'read'],
|
|
94
|
+
['blame', 'read'],
|
|
95
|
+
['cat-file', 'read'],
|
|
96
|
+
['cherry', 'read'], // lists +/- patch-equivalence vs an upstream; no mutation
|
|
97
|
+
['grep', 'read'],
|
|
98
|
+
['shortlog', 'read'],
|
|
99
|
+
['count-objects', 'read'],
|
|
100
|
+
['fsck', 'read'],
|
|
101
|
+
['var', 'read'],
|
|
102
|
+
['version', 'read'],
|
|
103
|
+
['--version', 'read'],
|
|
104
|
+
['help', 'read'],
|
|
105
|
+
['write-tree', 'read'],
|
|
106
|
+
['interpret-trailers', 'read'],
|
|
107
|
+
['check-ref-format', 'read'],
|
|
108
|
+
['check-ignore', 'read'], // exit code is the answer: 0 matched, 1 no match, 128 git failed
|
|
109
|
+
['symbolic-ref', 'read'],
|
|
110
|
+
['show-ref', 'read'],
|
|
111
|
+
['for-each-ref', 'read'],
|
|
112
|
+
['merge-base', 'read'],
|
|
113
|
+
['hash-object', 'read'],
|
|
114
|
+
// Both forms; isReadOnlyShape decides.
|
|
115
|
+
['branch', 'shape'], // bare `branch <name>` writes; --list / -l / --show-current / -v read
|
|
116
|
+
['stash', 'shape'], // `stash list` / `stash show` read
|
|
117
|
+
['worktree', 'shape'], // `worktree list` reads
|
|
118
|
+
['remote', 'shape'], // `remote get-url` / `remote show` read
|
|
119
|
+
['config', 'shape'], // `config --get` reads
|
|
120
|
+
['format-patch', 'shape'], // --inline writes
|
|
121
|
+
['reflog', 'shape'], // bare / show / exists read; everything else goes through execSync
|
|
122
|
+
];
|
|
123
|
+
export const GIT_VERB_CLASSES = new Map(GIT_VERB_CLASS_ENTRIES);
|
|
124
|
+
/** Verbs a write surface can run: `write` and `shape`. Derived from the table. */
|
|
125
|
+
export const DESTRUCTIVE_GIT_VERBS = new Set(GIT_VERB_CLASS_ENTRIES.filter(([, cls]) => cls !== 'read').map(([verb]) => verb));
|
|
86
126
|
/**
|
|
87
|
-
*
|
|
127
|
+
* Verbs a read surface can run: `read` and `shape`. Derived from the table.
|
|
128
|
+
* Membership is the funnel's classification, NOT proof the verb writes nothing:
|
|
129
|
+
* some forms of these verbs can write or run a program (EVO-029 items 4 and 5).
|
|
88
130
|
*/
|
|
89
|
-
export const READONLY_GIT_VERBS = new Set([
|
|
90
|
-
'status',
|
|
91
|
-
'log',
|
|
92
|
-
'diff',
|
|
93
|
-
'show',
|
|
94
|
-
'rev-parse',
|
|
95
|
-
'rev-list',
|
|
96
|
-
'ls-files',
|
|
97
|
-
'ls-tree',
|
|
98
|
-
'ls-remote',
|
|
99
|
-
'describe',
|
|
100
|
-
'name-rev',
|
|
101
|
-
'blame',
|
|
102
|
-
'cat-file',
|
|
103
|
-
'cherry', // read-only: lists +/- patch-equivalence vs an upstream; no mutation
|
|
104
|
-
'grep',
|
|
105
|
-
'shortlog',
|
|
106
|
-
'count-objects',
|
|
107
|
-
'fsck',
|
|
108
|
-
'var',
|
|
109
|
-
'version',
|
|
110
|
-
'--version',
|
|
111
|
-
'help',
|
|
112
|
-
'write-tree',
|
|
113
|
-
'interpret-trailers',
|
|
114
|
-
'check-ref-format',
|
|
115
|
-
'symbolic-ref',
|
|
116
|
-
'show-ref',
|
|
117
|
-
'for-each-ref',
|
|
118
|
-
'merge-base',
|
|
119
|
-
'reflog', // read-only by default; reflog expire/delete is destructive (caller must use execSync)
|
|
120
|
-
'hash-object',
|
|
121
|
-
'config', // overlap with destructive set; readSync shape-check enforces --get only
|
|
122
|
-
'remote', // overlap; readSync shape-check enforces list/get-url only
|
|
123
|
-
'branch', // overlap; readSync shape-check enforces --list / -l / --show-current / -v
|
|
124
|
-
'worktree', // overlap; readSync shape-check enforces list only
|
|
125
|
-
'format-patch', // overlap; readSync shape-check rejects --inline
|
|
126
|
-
'stash', // shape-check: `stash list` / `stash show` allowed
|
|
127
|
-
]);
|
|
131
|
+
export const READONLY_GIT_VERBS = new Set(GIT_VERB_CLASS_ENTRIES.filter(([, cls]) => cls !== 'write').map(([verb]) => verb));
|
|
128
132
|
/**
|
|
129
133
|
* Verbs that are "data-pull": they touch the object database and FETCH_HEAD
|
|
130
134
|
* but DO NOT modify the working tree, committed refs (heads/tags/remotes),
|
|
@@ -156,6 +160,7 @@ export const SOURCE_TREE_READ_TIER_VERBS = new Set([
|
|
|
156
160
|
'status',
|
|
157
161
|
'cherry',
|
|
158
162
|
'ls-files', // reaper checks ignored files before allowing worktree removal
|
|
163
|
+
'check-ignore', // reads ignore rules; a caller may need the answer on the source tree itself
|
|
159
164
|
]);
|
|
160
165
|
// ── Env denylist ────────────────────────────────────────────────────
|
|
161
166
|
const GIT_ENV_DENYLIST = new Set([
|
|
@@ -657,6 +662,19 @@ function isReadOnlyShape(verb, verbArgs) {
|
|
|
657
662
|
return null;
|
|
658
663
|
}
|
|
659
664
|
}
|
|
665
|
+
/**
|
|
666
|
+
* The one answer to "what does the funnel class this invocation as". `read`
|
|
667
|
+
* means the funnel classifies it as a read, not that git will write nothing.
|
|
668
|
+
* A verb outside GIT_VERB_CLASSES is `unclassified`, never read or write.
|
|
669
|
+
*/
|
|
670
|
+
export function classifyGitInvocation(verb, verbArgs) {
|
|
671
|
+
const cls = GIT_VERB_CLASSES.get(verb);
|
|
672
|
+
if (cls === undefined)
|
|
673
|
+
return 'unclassified';
|
|
674
|
+
if (cls === 'shape')
|
|
675
|
+
return isReadOnlyShape(verb, verbArgs) === false ? 'write' : 'read';
|
|
676
|
+
return cls;
|
|
677
|
+
}
|
|
660
678
|
/** Return the verbArgs (slice after the verb), accounting for pre-verb flags. */
|
|
661
679
|
function sliceAfterVerb(args, verb) {
|
|
662
680
|
const idx = args.indexOf(verb);
|
|
@@ -924,19 +942,23 @@ export class SafeGitExecutor {
|
|
|
924
942
|
* Synchronous destructive git execution.
|
|
925
943
|
*
|
|
926
944
|
* 1. Extract the verb and ALL target directories from args + opts.cwd.
|
|
927
|
-
* 2.
|
|
928
|
-
* 3.
|
|
929
|
-
*
|
|
930
|
-
*
|
|
931
|
-
* 5.
|
|
945
|
+
* 2. Refuse an unclassified verb, before anything else, on every surface.
|
|
946
|
+
* 3. Canonicalize each target and call assertNotInstarSourceTree on each.
|
|
947
|
+
* 4. Verify the invocation classifies as a write. Reads throw — callers must
|
|
948
|
+
* use readSync.
|
|
949
|
+
* 5. Strip env denylist; inject GIT_CONFIG_GLOBAL/SYSTEM=/dev/null.
|
|
950
|
+
* 6. Spawn git via execFileSync; return stdout.
|
|
932
951
|
*
|
|
933
|
-
* Throws SourceTreeGuardError if any target is the instar source tree
|
|
952
|
+
* Throws SourceTreeGuardError if any target is the instar source tree and the
|
|
953
|
+
* verb is classified.
|
|
934
954
|
* Throws SafeGitExecutorError on classification mismatch or arg errors.
|
|
935
955
|
*/
|
|
936
956
|
static execSync(args, opts) {
|
|
937
957
|
const { verb, targets: rawTargets } = extractVerbAndTargets(args, opts.cwd);
|
|
938
958
|
const targets = withLocalVerifyTargets(rawTargets, opts);
|
|
939
959
|
const verbArgs = sliceAfterVerb(args, verb);
|
|
960
|
+
const invocation = classifyGitInvocation(verb, verbArgs);
|
|
961
|
+
refuseUnclassified(invocation, verb, opts, targets);
|
|
940
962
|
// Run source-tree assertion against every target — unless the caller
|
|
941
963
|
// explicitly opted into the narrow data-pull allowlist (fetch / etc.)
|
|
942
964
|
// for the documented LAYER B + LAYER C canonical-ref read path. See
|
|
@@ -948,14 +970,14 @@ export class SafeGitExecutor {
|
|
|
948
970
|
audit('git', opts.operation, verb, targets[0], 'allowed', 'sourceTree-bypass');
|
|
949
971
|
}
|
|
950
972
|
// Verb classification.
|
|
951
|
-
const
|
|
952
|
-
if (
|
|
973
|
+
const shapeChecked = GIT_VERB_CLASSES.get(verb) === 'shape';
|
|
974
|
+
if (invocation === 'read' && shapeChecked) {
|
|
953
975
|
// The verb is ambiguous and the shape is read-only — caller used the
|
|
954
976
|
// wrong method. Fail loud rather than silently allow.
|
|
955
977
|
audit('git', opts.operation, verb, targets[0], 'denied', 'read-only-shape-via-execSync');
|
|
956
978
|
throw new SafeGitExecutorError(`SafeGitExecutor.execSync called with read-only shape '${verb} ${verbArgs.join(' ')}' — use SafeGitExecutor.readSync instead.`);
|
|
957
979
|
}
|
|
958
|
-
if (
|
|
980
|
+
if (invocation === 'read') {
|
|
959
981
|
// Pure read-only verb routed through execSync.
|
|
960
982
|
audit('git', opts.operation, verb, targets[0], 'denied', 'readonly-verb-via-execSync');
|
|
961
983
|
throw new SafeGitExecutorError(`SafeGitExecutor.execSync called with read-only verb '${verb}' — use SafeGitExecutor.readSync instead.`);
|
|
@@ -987,18 +1009,20 @@ export class SafeGitExecutor {
|
|
|
987
1009
|
const { verb, targets: rawTargets } = extractVerbAndTargets(args, opts.cwd);
|
|
988
1010
|
const targets = withLocalVerifyTargets(rawTargets, opts);
|
|
989
1011
|
const verbArgs = sliceAfterVerb(args, verb);
|
|
1012
|
+
const invocation = classifyGitInvocation(verb, verbArgs);
|
|
1013
|
+
refuseUnclassified(invocation, verb, opts, targets);
|
|
990
1014
|
if (!isSourceTreeCheckBypassed(verb, verbArgs, opts, targets)) {
|
|
991
1015
|
runSourceTreeChecks(targets, opts.operation, 'git', verb);
|
|
992
1016
|
}
|
|
993
1017
|
else {
|
|
994
1018
|
audit('git', opts.operation, verb, targets[0], 'allowed', 'sourceTree-bypass');
|
|
995
1019
|
}
|
|
996
|
-
const
|
|
997
|
-
if (
|
|
1020
|
+
const shapeChecked = GIT_VERB_CLASSES.get(verb) === 'shape';
|
|
1021
|
+
if (invocation === 'read' && shapeChecked) {
|
|
998
1022
|
audit('git', opts.operation, verb, targets[0], 'denied', 'read-only-shape-via-spawn');
|
|
999
1023
|
throw new SafeGitExecutorError(`SafeGitExecutor.spawn called with read-only shape '${verb}' — use SafeGitExecutor.readSync instead.`);
|
|
1000
1024
|
}
|
|
1001
|
-
if (
|
|
1025
|
+
if (invocation === 'read') {
|
|
1002
1026
|
audit('git', opts.operation, verb, targets[0], 'denied', 'readonly-verb-via-spawn');
|
|
1003
1027
|
throw new SafeGitExecutorError(`SafeGitExecutor.spawn called with read-only verb '${verb}' — use SafeGitExecutor.readSync instead.`);
|
|
1004
1028
|
}
|
|
@@ -1020,12 +1044,14 @@ export class SafeGitExecutor {
|
|
|
1020
1044
|
const { verb, targets: rawTargets } = extractVerbAndTargets(args, opts.cwd);
|
|
1021
1045
|
const targets = withLocalVerifyTargets(rawTargets, opts);
|
|
1022
1046
|
const verbArgs = sliceAfterVerb(args, verb);
|
|
1023
|
-
const
|
|
1024
|
-
|
|
1047
|
+
const invocation = classifyGitInvocation(verb, verbArgs);
|
|
1048
|
+
refuseUnclassified(invocation, verb, opts, targets);
|
|
1049
|
+
const shapeChecked = GIT_VERB_CLASSES.get(verb) === 'shape';
|
|
1050
|
+
if (invocation === 'write' && shapeChecked) {
|
|
1025
1051
|
audit('git', opts.operation, verb, targets[0], 'denied', 'destructive-shape-via-readStream');
|
|
1026
1052
|
throw new SafeGitExecutorError(`SafeGitExecutor.readStream called with destructive shape '${verb} ${verbArgs.join(' ')}' — use SafeGitExecutor.spawn instead.`);
|
|
1027
1053
|
}
|
|
1028
|
-
if (
|
|
1054
|
+
if (invocation === 'write') {
|
|
1029
1055
|
audit('git', opts.operation, verb, targets[0], 'denied', 'destructive-verb-via-readStream');
|
|
1030
1056
|
throw new SafeGitExecutorError(`SafeGitExecutor.readStream called with destructive verb '${verb}' — use SafeGitExecutor.spawn instead.`);
|
|
1031
1057
|
}
|
|
@@ -1058,12 +1084,14 @@ export class SafeGitExecutor {
|
|
|
1058
1084
|
// misusing this method get a clear "use execSync" error rather than
|
|
1059
1085
|
// a guard error first.
|
|
1060
1086
|
const verbArgs = sliceAfterVerb(args, verb);
|
|
1061
|
-
const
|
|
1062
|
-
|
|
1087
|
+
const invocation = classifyGitInvocation(verb, verbArgs);
|
|
1088
|
+
refuseUnclassified(invocation, verb, opts, targets);
|
|
1089
|
+
const shapeChecked = GIT_VERB_CLASSES.get(verb) === 'shape';
|
|
1090
|
+
if (invocation === 'write' && shapeChecked) {
|
|
1063
1091
|
audit('git', opts.operation, verb, targets[0], 'denied', 'destructive-shape-via-readSync');
|
|
1064
1092
|
throw new SafeGitExecutorError(`SafeGitExecutor.readSync called with destructive shape '${verb} ${verbArgs.join(' ')}' — use SafeGitExecutor.execSync instead.`);
|
|
1065
1093
|
}
|
|
1066
|
-
if (
|
|
1094
|
+
if (invocation === 'write') {
|
|
1067
1095
|
audit('git', opts.operation, verb, targets[0], 'denied', 'destructive-verb-via-readSync');
|
|
1068
1096
|
throw new SafeGitExecutorError(`SafeGitExecutor.readSync called with destructive verb '${verb}' — use SafeGitExecutor.execSync instead.`);
|
|
1069
1097
|
}
|
|
@@ -1098,30 +1126,29 @@ export class SafeGitExecutor {
|
|
|
1098
1126
|
return stdout || '';
|
|
1099
1127
|
}
|
|
1100
1128
|
/**
|
|
1101
|
-
* Verb-aware dispatcher.
|
|
1102
|
-
*
|
|
1103
|
-
* args and don't know at the
|
|
1129
|
+
* Verb-aware dispatcher. A write goes to execSync and everything else to
|
|
1130
|
+
* readSync, which extracts the verb itself and makes the real decision. Use
|
|
1131
|
+
* this from generic git() helpers that take dynamic args and don't know at the
|
|
1132
|
+
* call site whether the verb is destructive.
|
|
1133
|
+
*
|
|
1134
|
+
* It reads args[0] as the verb, so a pre-verb flag such as `-c` classifies as
|
|
1135
|
+
* unclassified here and lands on readSync (EVO-029 item 1).
|
|
1104
1136
|
*/
|
|
1105
1137
|
static run(args, opts) {
|
|
1106
1138
|
const verb = args[0];
|
|
1107
|
-
if (verb &&
|
|
1108
|
-
|
|
1109
|
-
const shape = isReadOnlyShape(verb, verbArgs);
|
|
1110
|
-
if (shape !== false)
|
|
1111
|
-
return SafeGitExecutor.readSync(args, opts);
|
|
1112
|
-
}
|
|
1113
|
-
if (verb && !DESTRUCTIVE_GIT_VERBS.has(verb)) {
|
|
1114
|
-
return SafeGitExecutor.readSync(args, opts);
|
|
1115
|
-
}
|
|
1116
|
-
if (verb) {
|
|
1117
|
-
const verbArgs = args.slice(1);
|
|
1118
|
-
const shape = isReadOnlyShape(verb, verbArgs);
|
|
1119
|
-
if (shape === true)
|
|
1120
|
-
return SafeGitExecutor.readSync(args, opts);
|
|
1139
|
+
if (verb && classifyGitInvocation(verb, args.slice(1)) === 'write') {
|
|
1140
|
+
return SafeGitExecutor.execSync(args, opts);
|
|
1121
1141
|
}
|
|
1122
|
-
return SafeGitExecutor.
|
|
1142
|
+
return SafeGitExecutor.readSync(args, opts);
|
|
1123
1143
|
}
|
|
1124
1144
|
}
|
|
1145
|
+
/** The one refusal for a verb outside GIT_VERB_CLASSES. Every surface calls it first. */
|
|
1146
|
+
function refuseUnclassified(invocation, verb, opts, targets) {
|
|
1147
|
+
if (invocation !== 'unclassified')
|
|
1148
|
+
return;
|
|
1149
|
+
audit('git', opts.operation, verb, targets[0], 'denied', 'unclassified-verb');
|
|
1150
|
+
throw new SafeGitExecutorError(`SafeGitExecutor: git verb '${verb}' is not classified. It is not in GIT_VERB_CLASSES, so no surface will run it. Adding a verb is a reviewed change to that table.`);
|
|
1151
|
+
}
|
|
1125
1152
|
function isSourceTreeCheckBypassed(verb, verbArgs, opts, targets = []) {
|
|
1126
1153
|
if (opts.sourceTreeReadOk && SOURCE_TREE_READ_TIER_VERBS.has(verb))
|
|
1127
1154
|
return true;
|
|
@@ -1372,6 +1399,7 @@ function audit(executor, operation, verb, target, outcome, reason) {
|
|
|
1372
1399
|
export const _internal = {
|
|
1373
1400
|
extractVerbAndTargets,
|
|
1374
1401
|
isReadOnlyShape,
|
|
1402
|
+
GIT_VERB_CLASS_ENTRIES,
|
|
1375
1403
|
sanitizeEnv,
|
|
1376
1404
|
GIT_ENV_DENYLIST,
|
|
1377
1405
|
repoHasLocalIdentity,
|