evolcore 0.0.19 → 0.0.21
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 +43 -1
- package/README.md +60 -9
- package/bin/install-codex-managed-hooks.mjs +61 -0
- package/dist/agents/baseagent.js +10 -6
- package/dist/agents/claude-runner.js +383 -111
- package/dist/agents/codex-app-server-client.js +10 -2
- package/dist/agents/codex-runner.js +405 -137
- package/dist/agents/ecagent-runner.js +174 -63
- package/dist/agents/gemini-runner.js +130 -30
- package/dist/agents/request-identity.js +25 -0
- package/dist/agents/runner-types.js +19 -0
- package/dist/aun/aid/agentmd.js +66 -2
- package/dist/aun/aid/identity.js +4 -1
- package/dist/aun/aid/index.js +1 -1
- package/dist/aun/msg/group.js +86 -9
- package/dist/aun/msg/history.js +213 -36
- package/dist/aun/msg/managed-operation.js +58 -9
- package/dist/aun/msg/p2p.js +26 -11
- package/dist/aun/outbox.js +295 -68
- package/dist/aun/service-proxy.js +43 -25
- package/dist/channels/aun.js +1004 -273
- package/dist/channels/daemon.js +6 -1
- package/dist/cli/agent-command.js +4 -3
- package/dist/cli/agent.js +66 -56
- package/dist/cli/aun-commands.js +177 -42
- package/dist/cli/command-log.js +10 -11
- package/dist/cli/contact.js +1 -0
- package/dist/cli/daemon-commands.js +81 -121
- package/dist/cli/index.js +1 -0
- package/dist/cli/init.js +76 -24
- package/dist/cli/restart-monitor.js +3 -3
- package/dist/cli/task-context.js +46 -0
- package/dist/cli/trigger-command.js +1 -1
- package/dist/cli/watch-logs.js +10 -3
- package/dist/config/builtin-roles.js +1 -0
- package/dist/config/config-field-policy.js +16 -5
- package/dist/config/config-manager.js +226 -24
- package/dist/config/config-operation-service.js +1 -2
- package/dist/config/contact-operation-service.js +32 -1
- package/dist/config/contact-request-service.js +44 -0
- package/dist/config/daemon-services.js +186 -0
- package/dist/config/gateway-config.js +29 -16
- package/dist/config/lifecycle.js +16 -5
- package/dist/config/role-service.js +54 -3
- package/dist/config/schema-migration.js +550 -0
- package/dist/config-store.js +161 -12
- package/dist/core/agent-application-service.js +279 -0
- package/dist/core/audit/log-integrity.js +102 -0
- package/dist/core/auth/agent-delegation.js +31 -1
- package/dist/core/auth/auth-gateway.js +33 -4
- package/dist/core/auth/authorization-audit.js +155 -4
- package/dist/core/auth/operation-authorizer.js +41 -1
- package/dist/core/auth/operation-catalog.js +9 -1
- package/dist/core/bootstrap-messages.js +2 -2
- package/dist/core/bootstrap-service.js +27 -38
- package/dist/core/causation/aun-association.js +7 -4
- package/dist/core/channel-loader.js +0 -2
- package/dist/core/command/agent-control.js +56 -16
- package/dist/core/command/command-handler.js +290 -44
- package/dist/core/command/connect-menu.js +3 -4
- package/dist/core/command/group-menu.js +5 -7
- package/dist/core/command/menu-handler.js +279 -80
- package/dist/core/command/role-menu.js +21 -11
- package/dist/core/command/slash-gate.js +85 -18
- package/dist/core/command/slash-handler.js +350 -32
- package/dist/core/event-catalog.js +5 -0
- package/dist/core/evolagent.js +9 -4
- package/dist/core/handoff/dispatcher.js +4 -0
- package/dist/core/handoff/runtime.js +10 -0
- package/dist/core/handoff/store.js +32 -9
- package/dist/core/inference/text-inference.js +7 -15
- package/dist/core/message/im-renderer.js +83 -84
- package/dist/core/message/{inbound-admission.js → message-admission.js} +51 -1
- package/dist/core/message/message-bridge.js +124 -10
- package/dist/core/message/message-log.js +14 -7
- package/dist/core/message/message-queue.js +206 -16
- package/dist/core/message/message-utils.js +12 -5
- package/dist/core/message/response-engine.js +495 -72
- package/dist/core/message/send-receipt.js +1 -0
- package/dist/core/message/stream-debouncer.js +9 -2
- package/dist/core/model/model-catalog.js +23 -15
- package/dist/core/model/model-diagnostics.js +28 -10
- package/dist/core/permission/approval-gateway.js +180 -6
- package/dist/core/permission/ec-command-parser.js +465 -56
- package/dist/core/permission/mode.js +18 -3
- package/dist/core/{protected-paths.js → permission/protected-paths.js} +17 -5
- package/dist/core/permission/readonly-shell-query.js +263 -9
- package/dist/core/permission/sandbox-runtime.js +205 -13
- package/dist/core/permission/tool-policy.js +673 -62
- package/dist/core/relation/peer-identity.js +18 -0
- package/dist/core/session/session-fs-store.js +154 -5
- package/dist/core/session/session-manager.js +299 -30
- package/dist/core/session/session-renew.js +19 -12
- package/dist/core/session/session-turn-coordinator.js +11 -4
- package/dist/eck/kit-renderer.js +18 -9
- package/dist/index.js +283 -65
- package/dist/ipc.js +374 -24
- package/dist/paths.js +64 -7
- package/dist/response-system/context-builder.js +1 -7
- package/dist/trigger/anomaly-store.js +1 -0
- package/dist/trigger/feedback.js +56 -5
- package/dist/trigger/history.js +79 -4
- package/dist/trigger/legacy-session-history.js +2 -2
- package/dist/trigger/parser.js +3 -2
- package/dist/trigger/validation.js +6 -1
- package/dist/utils/atomic-write.js +27 -0
- package/dist/utils/ecweb-utils.js +16 -2
- package/dist/utils/error-utils.js +4 -1
- package/dist/utils/logger.js +21 -2
- package/dist/utils/process-tree-stats.js +24 -4
- package/dist/utils/process-tree-worker.js +31 -0
- package/dist/utils/project-path.js +1 -2
- package/dist/utils/stats.js +52 -18
- package/dist/utils/welcome.js +2 -2
- package/kits/docs/INDEX.md +1 -1
- package/kits/docs/evolcore/INDEX.md +1 -1
- package/kits/docs/evolcore/contact.md +7 -1
- package/kits/docs/evolcore/msg.md +16 -0
- package/kits/rules/01-overview.md +1 -1
- package/kits/rules/03-identity.md +1 -1
- package/kits/rules/05-venue.md +1 -1
- package/kits/schemas/_meta.json +10 -5
- package/kits/schemas/agent-config.schema.10.json +2 -1
- package/kits/schemas/agent-config.schema.11.json +421 -0
- package/kits/schemas/daemon.schema.5.json +135 -0
- package/kits/schemas/daemon.schema.6.json +131 -0
- package/kits/schemas/defaults.schema.5.json +119 -0
- package/kits/schemas/migrations/README.md +3 -1
- package/kits/schemas/relation-config.schema.8.json +13 -0
- package/kits/schemas/role-config.schema.1.json +1 -2
- package/kits/schemas/single-session.schema.3.json +32 -0
- package/kits/templates/roles/admin.json +1 -0
- package/kits/templates/roles/member.json +1 -0
- package/kits/templates/roles/visitor.json +1 -0
- package/package.json +7 -3
- package/skills/eclink/SKILL.md +2 -0
|
@@ -1,8 +1,22 @@
|
|
|
1
1
|
export const PUBLIC_PERMISSION_MODES = ['readonly', 'auto', 'request', 'bypass'];
|
|
2
|
+
export const FULL_ACCESS_PERMISSION_MODE = 'fullaccess';
|
|
2
3
|
export function isPublicPermissionMode(value) {
|
|
3
4
|
return typeof value === 'string'
|
|
4
5
|
&& PUBLIC_PERMISSION_MODES.includes(value);
|
|
5
6
|
}
|
|
7
|
+
export function isExecutionPermissionMode(value) {
|
|
8
|
+
return value === FULL_ACCESS_PERMISSION_MODE || isPublicPermissionMode(value);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Normalize a trusted execution-call mode without exposing fullaccess as a
|
|
12
|
+
* role/session configuration value. Legacy persisted public values retain the
|
|
13
|
+
* same migration behavior as normalizePermissionMode().
|
|
14
|
+
*/
|
|
15
|
+
export function normalizeExecutionPermissionMode(value) {
|
|
16
|
+
if (value === FULL_ACCESS_PERMISSION_MODE)
|
|
17
|
+
return FULL_ACCESS_PERMISSION_MODE;
|
|
18
|
+
return normalizePermissionMode(value).mode;
|
|
19
|
+
}
|
|
6
20
|
/**
|
|
7
21
|
* Normalize persisted legacy values at the runtime boundary.
|
|
8
22
|
*
|
|
@@ -27,13 +41,14 @@ export function normalizePermissionMode(value) {
|
|
|
27
41
|
* owner running in bypass mode. Every other combination remains fail-closed.
|
|
28
42
|
*/
|
|
29
43
|
export function resolvePhaseOneExecutionSandbox(permissionMode, role) {
|
|
30
|
-
const normalizedMode =
|
|
44
|
+
const normalizedMode = normalizeExecutionPermissionMode(permissionMode);
|
|
31
45
|
const normalizedRole = role ?? undefined;
|
|
32
|
-
const
|
|
46
|
+
const fullAccess = normalizedMode === FULL_ACCESS_PERMISSION_MODE;
|
|
47
|
+
const sandboxOff = fullAccess || (normalizedMode === 'bypass' && normalizedRole === 'owner');
|
|
33
48
|
return {
|
|
34
49
|
permissionMode: normalizedMode,
|
|
35
50
|
role: normalizedRole,
|
|
36
51
|
state: sandboxOff ? 'off' : 'active',
|
|
37
|
-
reason: sandboxOff ? 'owner-bypass' : 'phase-one-required',
|
|
52
|
+
reason: fullAccess ? 'fullaccess' : sandboxOff ? 'owner-bypass' : 'phase-one-required',
|
|
38
53
|
};
|
|
39
54
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import { resolveRoot } from '
|
|
3
|
+
import { resolveRoot } from '../../paths.js';
|
|
4
4
|
const H_CLASS_RELATIVE_PATTERNS = [
|
|
5
5
|
/^daemon\.json$/,
|
|
6
6
|
/^agents\/defaults\.json$/,
|
|
@@ -28,13 +28,13 @@ const H_CLASS_RELATIVE_PATTERNS = [
|
|
|
28
28
|
/^data\/outbox(?:\/|$)/,
|
|
29
29
|
/^data\/handoff(?:\/|$)/,
|
|
30
30
|
/^agents\/[^/]+\/sessions(?:\/|$)/,
|
|
31
|
-
/^agents\/[^/]+\/data\/(?:channels(?:\/|$)|handoff(?:\/|$)|outbox\.jsonl(?:_|__|\.tmp-.*)?$|contact-audit\.jsonl$)/,
|
|
31
|
+
/^agents\/[^/]+\/data\/(?:channels(?:\/|$)|handoff(?:\/|$)|(?:outbox|activity-outbox)\.jsonl(?:_|__|\.tmp-.*)?$|contact-audit\.jsonl$)/,
|
|
32
32
|
];
|
|
33
33
|
const H_CLASS_REFERENCE_PATTERNS = [
|
|
34
34
|
/(?:^|[^A-Za-z0-9_.-])daemon\.json(?=$|[^A-Za-z0-9_.-])/,
|
|
35
35
|
/(?:^|[^A-Za-z0-9_.-])agents[\\/](?:defaults\.json|[^/\\\s"']+[\\/](?:(?:config|contact)\.json|contact-operations\.jsonl|relations[\\/][^/\\\s"']+[\\/]config\.json))(?=$|[\s"';&|<>)},])/,
|
|
36
36
|
/(?:^|[^A-Za-z0-9_.-])(?:backups[\\/]config|\.snapshots|CA|(?:AIDs|aids)[\\/][^/\\\s"']+[\\/](?:cert|keys|private))(?:[\\/]|$|[\s"';&|<>)},])/,
|
|
37
|
-
/(?:^|[/\\\s"'=<>])(?:\.device_id|\.env|\.seed(?:\.[^/\\\s"']*)?|\.migrated-[^/\\\s"']*|\.lock|daemon\.pid|data[\\/](?:causation-aun\.json(?:_|__)?|sessions|contact-book-audit\.jsonl|feishu-seen-[^/\\\s"']+\.jsonl|[a-z0-9_-]+-greeted-[^/\\\s"']+\.jsonl|message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|daemon[\\/](?:message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|control|migrations)|channel-state|instance[\\/]control\.token|outbox|handoff)|agents[\\/][^/\\\s"']+[\\/](?:sessions|data[\\/](?:channels|handoff|outbox\.jsonl|contact-audit\.jsonl)))(?:[\\/]|$|[\s"';&|<>)},])/,
|
|
37
|
+
/(?:^|[/\\\s"'=<>])(?:\.device_id|\.env|\.seed(?:\.[^/\\\s"']*)?|\.migrated-[^/\\\s"']*|\.lock|daemon\.pid|data[\\/](?:causation-aun\.json(?:_|__)?|sessions|contact-book-audit\.jsonl|feishu-seen-[^/\\\s"']+\.jsonl|[a-z0-9_-]+-greeted-[^/\\\s"']+\.jsonl|message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|daemon[\\/](?:message-queue\.json(?:_|__|\.tmp-[^/\\\s"']*)?|control|migrations)|channel-state|instance[\\/]control\.token|outbox|handoff)|agents[\\/][^/\\\s"']+[\\/](?:sessions|data[\\/](?:channels|handoff|(?:outbox|activity-outbox)\.jsonl|contact-audit\.jsonl)))(?:[\\/]|$|[\s"';&|<>)},])/,
|
|
38
38
|
/(?:^|[/\\\s"'=<>])data[\\/]triggers(?:[\\/]|$|[\s"';&|<>)},])/,
|
|
39
39
|
/(?:^|[/\\\s"'=<>])(?:[^/\\\s"']+\.json_|[^/\\\s"']+\.json\.migrated|defaults_\d+\.json)(?=$|[\s"';&|<>)},])/,
|
|
40
40
|
];
|
|
@@ -287,6 +287,7 @@ export function getHClassSandboxPatterns(root = resolveRoot()) {
|
|
|
287
287
|
path.join('agents', '*', 'data', 'channels', '**'),
|
|
288
288
|
path.join('agents', '*', 'data', 'handoff', '**'),
|
|
289
289
|
path.join('agents', '*', 'data', 'outbox.jsonl*'),
|
|
290
|
+
path.join('agents', '*', 'data', 'activity-outbox.jsonl*'),
|
|
290
291
|
path.join('agents', '*', 'data', 'contact-audit.jsonl'),
|
|
291
292
|
];
|
|
292
293
|
return entries.map(entry => path.join(absoluteRoot, entry));
|
|
@@ -369,6 +370,7 @@ export function getExistingHClassMaskTargets(root = resolveRoot()) {
|
|
|
369
370
|
addExistingTarget(targets, path.join(agentRoot, 'data', 'channels'));
|
|
370
371
|
addExistingTarget(targets, path.join(agentRoot, 'data', 'handoff'));
|
|
371
372
|
addExistingTarget(targets, path.join(agentRoot, 'data', 'outbox.jsonl'));
|
|
373
|
+
addExistingTarget(targets, path.join(agentRoot, 'data', 'activity-outbox.jsonl'));
|
|
372
374
|
addExistingTarget(targets, path.join(agentRoot, 'data', 'contact-audit.jsonl'));
|
|
373
375
|
}
|
|
374
376
|
}
|
|
@@ -435,12 +437,22 @@ export function buildCodexHClassFilesystemRules(root = resolveRoot()) {
|
|
|
435
437
|
export function buildCodexProtectedFilesystemRules(root = resolveRoot(), options = {}) {
|
|
436
438
|
const lClassRules = {};
|
|
437
439
|
const lClassAccess = options.denyLClassRead ? 'deny' : 'read';
|
|
440
|
+
// Codex's Linux sandbox expands glob deny rules into per-file mount targets.
|
|
441
|
+
// When an outer H-class guard has already mounted the L-class directory
|
|
442
|
+
// read-only, that expansion can race with the mount and fail before the
|
|
443
|
+
// command starts (for example while creating triggers/history.jsonl).
|
|
444
|
+
// Directory-level deny is sufficient for the policy bridge and avoids the
|
|
445
|
+
// nested bwrap per-file projection. Callers on platforms without that
|
|
446
|
+
// outer guard retain the historical expansion by default.
|
|
447
|
+
const expandLClassReadDeny = options.expandLClassReadDeny ?? true;
|
|
438
448
|
for (const target of getExistingLClassReadOnlyTargets(root)) {
|
|
439
449
|
if (target.kind === 'directory') {
|
|
440
|
-
if (options.denyLClassRead || path.basename(target.path) === 'triggers') {
|
|
450
|
+
if (!expandLClassReadDeny || options.denyLClassRead || path.basename(target.path) === 'triggers') {
|
|
441
451
|
lClassRules[target.path] = lClassAccess;
|
|
442
452
|
}
|
|
443
|
-
|
|
453
|
+
if (expandLClassReadDeny || !options.denyLClassRead) {
|
|
454
|
+
lClassRules[path.join(target.path, '**')] = lClassAccess;
|
|
455
|
+
}
|
|
444
456
|
}
|
|
445
457
|
else {
|
|
446
458
|
lClassRules[target.path] = lClassAccess;
|
|
@@ -3,7 +3,8 @@ import { parseLiteralShellArgv } from './ec-command-parser.js';
|
|
|
3
3
|
const MAX_QUERY_LISTS = 8;
|
|
4
4
|
const MAX_PIPELINE_STAGES = 3;
|
|
5
5
|
const SUPPORTED_EXECUTABLES = new Set([
|
|
6
|
-
'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort', 'git', 'wc', 'stat', 'lstat',
|
|
6
|
+
'echo', 'pwd', 'ls', 'cat', 'grep', 'rg', 'find', 'sed', 'head', 'sort', 'git', 'wc', 'stat', 'lstat', 'awk',
|
|
7
|
+
'bash', 'sh',
|
|
7
8
|
]);
|
|
8
9
|
const FIND_MUTATING_PRIMARY_RE = /^-(?:delete|exec|execdir|ok|okdir|fprint|fprint0|fprintf|fls)$/;
|
|
9
10
|
const RG_PROCESS_SPAWNING_OPTION_RE = /^(?:--pre|--hostname-bin|--pre-glob|--search-zip)(?:=|$)/;
|
|
@@ -113,6 +114,22 @@ function exact(value) {
|
|
|
113
114
|
function recursive(value) {
|
|
114
115
|
return { value, access: 'recursive' };
|
|
115
116
|
}
|
|
117
|
+
function isLiteralGitPathspec(value) {
|
|
118
|
+
// Git pathspec magic and wildcards can expand a seemingly harmless token to
|
|
119
|
+
// protected files elsewhere in the repository. The readonly diagnostic
|
|
120
|
+
// capability accepts only a literal path or directory scope.
|
|
121
|
+
return !!value
|
|
122
|
+
&& !value.startsWith('-')
|
|
123
|
+
&& !value.startsWith(':')
|
|
124
|
+
&& !/[\0*?[\\]/.test(value);
|
|
125
|
+
}
|
|
126
|
+
function isLikelyGitPath(value) {
|
|
127
|
+
// Before `--`, Git accepts both revisions and pathspecs. Only classify
|
|
128
|
+
// tokens with an unmistakable path shape here; bare names such as `HEAD`
|
|
129
|
+
// and `main` remain revision selectors and do not become filesystem scope.
|
|
130
|
+
return isLiteralGitPathspec(value)
|
|
131
|
+
&& (value === '.' || value === '..' || value.includes('/') || value.startsWith('.') || /\.[A-Za-z0-9_-]+$/.test(value));
|
|
132
|
+
}
|
|
116
133
|
function parseOptionsAndPaths(args, valueOptions, recursiveOptions = new Set()) {
|
|
117
134
|
const paths = [];
|
|
118
135
|
let optionsEnded = false;
|
|
@@ -472,9 +489,10 @@ function analyzeSort(args) {
|
|
|
472
489
|
/**
|
|
473
490
|
* Git metadata queries do not expose file contents and their output paths are
|
|
474
491
|
* controlled by the repository, not by arbitrary shell operands. Keep this
|
|
475
|
-
* deliberately small: status
|
|
476
|
-
* used for workspace diagnostics. Mutating
|
|
477
|
-
* unproven so
|
|
492
|
+
* deliberately small: status, bounded diff summaries, pickaxe log queries,
|
|
493
|
+
* and blame are the patterns used for workspace diagnostics. Mutating
|
|
494
|
+
* subcommands and unrecognised options remain unproven so normal H/L checks
|
|
495
|
+
* still apply.
|
|
478
496
|
*/
|
|
479
497
|
function analyzeGit(args) {
|
|
480
498
|
if (args.length === 0)
|
|
@@ -502,6 +520,7 @@ function analyzeGit(args) {
|
|
|
502
520
|
}
|
|
503
521
|
if (subcommand === 'diff') {
|
|
504
522
|
let sawSummary = false;
|
|
523
|
+
let noIndex = false;
|
|
505
524
|
let optionsEnded = false;
|
|
506
525
|
const paths = [];
|
|
507
526
|
const allowedFlags = new Set([
|
|
@@ -518,17 +537,152 @@ function analyzeGit(args) {
|
|
|
518
537
|
if (arg === '--stat' || arg === '--shortstat' || arg === '--numstat'
|
|
519
538
|
|| arg === '--name-only' || arg === '--name-status')
|
|
520
539
|
sawSummary = true;
|
|
540
|
+
if (arg === '--no-index')
|
|
541
|
+
noIndex = true;
|
|
521
542
|
if (!allowedFlags.has(arg) && !/^[0-9a-f]{7,64}$/.test(arg))
|
|
522
543
|
return null;
|
|
523
544
|
continue;
|
|
524
545
|
}
|
|
525
|
-
//
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
546
|
+
// A Git pathspec may name a directory or wildcard scope, so model it as
|
|
547
|
+
// recursive even when the token itself is literal. This keeps `-- .`
|
|
548
|
+
// from bypassing nested protected paths.
|
|
549
|
+
if (optionsEnded) {
|
|
550
|
+
if (!isLiteralGitPathspec(arg))
|
|
551
|
+
return null;
|
|
552
|
+
paths.push(recursive(arg));
|
|
553
|
+
}
|
|
554
|
+
else if (noIndex) {
|
|
555
|
+
if (!isLiteralGitPathspec(arg))
|
|
556
|
+
return null;
|
|
557
|
+
// `--no-index` compares two filesystem paths directly, so both
|
|
558
|
+
// operands must participate in workspace/H-class checks.
|
|
559
|
+
paths.push(recursive(arg));
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
if (!/^[0-9A-Za-z._~^/:-]+$/.test(arg))
|
|
563
|
+
return null;
|
|
564
|
+
if (isLikelyGitPath(arg))
|
|
565
|
+
paths.push(recursive(arg));
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!sawSummary)
|
|
569
|
+
return null;
|
|
570
|
+
if (noIndex && paths.length !== 2)
|
|
571
|
+
return null;
|
|
572
|
+
return paths;
|
|
573
|
+
}
|
|
574
|
+
if (subcommand === 'log') {
|
|
575
|
+
let optionsEnded = false;
|
|
576
|
+
let sawSearch = false;
|
|
577
|
+
const paths = [];
|
|
578
|
+
const valueOptions = new Set([
|
|
579
|
+
'-S', '-G', '--grep', '--author', '--committer', '--since', '--after', '--until', '--before',
|
|
580
|
+
'--format', '--pretty', '--max-count', '-n', '--skip', '--decorate-refs',
|
|
581
|
+
]);
|
|
582
|
+
const allowedFlags = new Set([
|
|
583
|
+
'--all', '--branches', '--tags', '--remotes', '--oneline', '--no-patch', '--no-decorate', '--decorate',
|
|
584
|
+
'--follow', '--pickaxe-all', '--pickaxe-regex', '--reverse', '--topo-order', '--date-order',
|
|
585
|
+
'--author-date-order', '--first-parent', '--no-merges', '--merges', '--stat', '--shortstat',
|
|
586
|
+
'--name-only', '--name-status', '--no-renames', '--no-ext-diff', '--no-textconv', '--no-color',
|
|
587
|
+
'--color=never',
|
|
588
|
+
]);
|
|
589
|
+
for (let index = 1; index < args.length; index++) {
|
|
590
|
+
const arg = args[index];
|
|
591
|
+
if (!optionsEnded && arg === '--') {
|
|
592
|
+
optionsEnded = true;
|
|
593
|
+
continue;
|
|
594
|
+
}
|
|
595
|
+
if (optionsEnded) {
|
|
596
|
+
if (!isLiteralGitPathspec(arg))
|
|
597
|
+
return null;
|
|
598
|
+
paths.push(recursive(arg));
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
if (arg.startsWith('-')) {
|
|
602
|
+
const option = arg.split('=', 1)[0];
|
|
603
|
+
if (arg === '-S' || arg === '-G' || /^-[SG].+/.test(arg)) {
|
|
604
|
+
sawSearch = true;
|
|
605
|
+
if (arg.length === 2) {
|
|
606
|
+
if (++index >= args.length)
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (valueOptions.has(option)) {
|
|
612
|
+
if (!arg.includes('=') && !arg.startsWith('-S') && !arg.startsWith('-G')) {
|
|
613
|
+
if (++index >= args.length)
|
|
614
|
+
return null;
|
|
615
|
+
}
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
if (!allowedFlags.has(arg) && !/^(?:[0-9A-Fa-f]{7,64}|HEAD(?:~[0-9]+)?|[A-Za-z0-9._/-]+\.\.\.?)$/.test(arg))
|
|
619
|
+
return null;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
// Revisions before `--` are metadata selectors, never filesystem paths.
|
|
623
|
+
if (!/^[A-Za-z0-9._~^/:@+-]+$/.test(arg))
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
return sawSearch && paths.length > 0 ? paths : null;
|
|
627
|
+
}
|
|
628
|
+
if (subcommand === 'blame') {
|
|
629
|
+
let optionsEnded = false;
|
|
630
|
+
let sawPath = false;
|
|
631
|
+
const paths = [];
|
|
632
|
+
const valueOptions = new Set(['-L', '--abbrev', '--date', '--ignore-rev']);
|
|
633
|
+
const pathValueOptions = new Set(['--ignore-revs-file', '--contents']);
|
|
634
|
+
const allowedFlags = new Set([
|
|
635
|
+
'--porcelain', '--line-porcelain', '--incremental', '--root', '--show-email', '--show-number',
|
|
636
|
+
'--show-name', '--reverse', '--progress', '--score-debug', '--encoding', '--no-progress',
|
|
637
|
+
]);
|
|
638
|
+
for (let index = 1; index < args.length; index++) {
|
|
639
|
+
const arg = args[index];
|
|
640
|
+
if (!optionsEnded && arg === '--') {
|
|
641
|
+
optionsEnded = true;
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
if (optionsEnded) {
|
|
645
|
+
if (!isLiteralGitPathspec(arg) || sawPath)
|
|
646
|
+
return null;
|
|
647
|
+
paths.push(exact(arg));
|
|
648
|
+
sawPath = true;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (arg.startsWith('-')) {
|
|
652
|
+
const option = arg.split('=', 1)[0];
|
|
653
|
+
if (option === '-L' || option === '-C') {
|
|
654
|
+
// `-L` takes a line range; `-C` is a repeatable flag with an
|
|
655
|
+
// optional score and therefore must not consume the following path.
|
|
656
|
+
if (option === '-L' && !arg.includes('=') && arg.length === 2
|
|
657
|
+
&& ++index >= args.length)
|
|
658
|
+
return null;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (pathValueOptions.has(option)) {
|
|
662
|
+
const value = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : args[++index];
|
|
663
|
+
if (!value)
|
|
664
|
+
return null;
|
|
665
|
+
paths.push(exact(value));
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
if (valueOptions.has(option)) {
|
|
669
|
+
if (!arg.includes('=') && ++index >= args.length)
|
|
670
|
+
return null;
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (!allowedFlags.has(arg))
|
|
674
|
+
return null;
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
// The common diagnostic form is `git blame <path>`. Keep one literal
|
|
678
|
+
// path operand; callers that need a revision should use `-- <path>` so
|
|
679
|
+
// the boundary is unambiguous to the policy parser.
|
|
680
|
+
if (sawPath || !isLiteralGitPathspec(arg))
|
|
529
681
|
return null;
|
|
682
|
+
paths.push(exact(arg));
|
|
683
|
+
sawPath = true;
|
|
530
684
|
}
|
|
531
|
-
return
|
|
685
|
+
return paths.length === 1 ? paths : null;
|
|
532
686
|
}
|
|
533
687
|
return null;
|
|
534
688
|
}
|
|
@@ -574,6 +728,99 @@ function analyzeStat(args) {
|
|
|
574
728
|
}
|
|
575
729
|
return paths.length > 0 ? paths : null;
|
|
576
730
|
}
|
|
731
|
+
function analyzeAwk(args) {
|
|
732
|
+
let index = 0;
|
|
733
|
+
while (index < args.length && args[index].startsWith('-')) {
|
|
734
|
+
const option = args[index];
|
|
735
|
+
if (option === '--') {
|
|
736
|
+
index++;
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
if (option === '-F') {
|
|
740
|
+
if (++index >= args.length)
|
|
741
|
+
return null;
|
|
742
|
+
index++;
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (option.startsWith('-F') && !option.startsWith('--') && option.length > 2) {
|
|
746
|
+
index++;
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (option === '-v' || option === '--assign') {
|
|
750
|
+
if (++index >= args.length)
|
|
751
|
+
return null;
|
|
752
|
+
index++;
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
755
|
+
if (option.startsWith('-v') && !option.startsWith('--') && option.length > 2) {
|
|
756
|
+
index++;
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
if (option === '--field-separator') {
|
|
760
|
+
if (++index >= args.length)
|
|
761
|
+
return null;
|
|
762
|
+
index++;
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
if (option.startsWith('--field-separator=') || option.startsWith('--assign=')) {
|
|
766
|
+
index++;
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const program = args[index++];
|
|
772
|
+
// Permit the common formatting/filtering form while excluding awk's
|
|
773
|
+
// execution, file-redirection and shell escape features.
|
|
774
|
+
if (!program || !/^\{[\s\S]*\}$/.test(program)
|
|
775
|
+
|| /\b(?:system|getline|close|nextfile)\b|(?:>>?|\|)|\b(?:delete|exit)\b/.test(program)
|
|
776
|
+
|| /(^|[^=!<>])=([^=]|$)/.test(program))
|
|
777
|
+
return null;
|
|
778
|
+
const paths = args.slice(index).filter(value => value !== '-').map(exact);
|
|
779
|
+
return paths;
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* `bash -n`/`sh -n` only parses a script and never executes it. Treat the
|
|
783
|
+
* script operands as exact reads, but keep every other shell option and
|
|
784
|
+
* nested command fail-closed. This is intentionally narrower than allowing
|
|
785
|
+
* arbitrary `bash -c`, which would re-introduce an opaque shell carrier.
|
|
786
|
+
*/
|
|
787
|
+
function analyzeShellSyntaxCheck(args) {
|
|
788
|
+
if (args.length < 2)
|
|
789
|
+
return null;
|
|
790
|
+
let index = 0;
|
|
791
|
+
let sawNoExec = false;
|
|
792
|
+
while (index < args.length && args[index].startsWith('-') && args[index] !== '-') {
|
|
793
|
+
const option = args[index++];
|
|
794
|
+
if (option === '--') {
|
|
795
|
+
// The increment above already consumed the delimiter; the following
|
|
796
|
+
// token is the script path, not another option.
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
if (option === '-n' || option === '--noexec') {
|
|
800
|
+
sawNoExec = true;
|
|
801
|
+
continue;
|
|
802
|
+
}
|
|
803
|
+
// `-n` may be combined with other bash flags, but only when the whole
|
|
804
|
+
// cluster is made of harmless parser flags. Do not accept flags that can
|
|
805
|
+
// select an inline command, source files, or alter execution semantics.
|
|
806
|
+
if (/^-[A-Za-z]*n[A-Za-z]*$/.test(option)
|
|
807
|
+
&& !/[cCeEirsSx]/.test(option.replace(/^-/, ''))) {
|
|
808
|
+
sawNoExec = true;
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
if (!sawNoExec || index >= args.length)
|
|
814
|
+
return null;
|
|
815
|
+
const paths = [];
|
|
816
|
+
for (; index < args.length; index++) {
|
|
817
|
+
const value = args[index];
|
|
818
|
+
if (!value || value === '-' || value.startsWith('-'))
|
|
819
|
+
return null;
|
|
820
|
+
paths.push(exact(value));
|
|
821
|
+
}
|
|
822
|
+
return paths;
|
|
823
|
+
}
|
|
577
824
|
function analyzeCommand(argv) {
|
|
578
825
|
const executable = normalizeExecutable(argv[0]);
|
|
579
826
|
if (!executable)
|
|
@@ -623,6 +870,13 @@ function analyzeCommand(argv) {
|
|
|
623
870
|
pathOperands = analyzeStat(args);
|
|
624
871
|
accessKind = 'metadata';
|
|
625
872
|
break;
|
|
873
|
+
case 'awk':
|
|
874
|
+
pathOperands = analyzeAwk(args);
|
|
875
|
+
break;
|
|
876
|
+
case 'bash':
|
|
877
|
+
case 'sh':
|
|
878
|
+
pathOperands = analyzeShellSyntaxCheck(args);
|
|
879
|
+
break;
|
|
626
880
|
}
|
|
627
881
|
if (!pathOperands)
|
|
628
882
|
return null;
|