dsh-perm-gate 2.1.2 → 3.0.1
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.ja.md +82 -0
- package/CHANGELOG.ko.md +80 -0
- package/CHANGELOG.md +210 -0
- package/INSTALL.ja.md +14 -8
- package/INSTALL.ko.md +15 -9
- package/INSTALL.md +16 -8
- package/INSTALL.zh.md +12 -7
- package/README.ja.md +19 -6
- package/README.ko.md +20 -7
- package/README.md +35 -4
- package/README.zh.md +13 -4
- package/docs/baseline-guard.md +56 -0
- package/docs/plan-rules-to-settings.md +217 -0
- package/docs/rules-format.en.md +294 -0
- package/docs/rules-format.md +279 -0
- package/docs/ui-gap-analysis-and-branch-permissions.md +338 -0
- package/dsh.plugin.json +2 -2
- package/examples/permissions.example.yaml +16 -1
- package/lib/allowlist.d.ts +27 -0
- package/lib/allowlist.js +103 -1
- package/lib/cli.js +11 -14
- package/lib/client.js +484 -6
- package/lib/client.js.map +1 -1
- package/lib/command-dispatcher.d.ts +22 -0
- package/lib/command-dispatcher.js +97 -0
- package/lib/command-semantics.d.ts +62 -0
- package/lib/command-semantics.js +6 -0
- package/lib/compiler.d.ts +11 -0
- package/lib/compiler.js +16 -0
- package/lib/config.d.ts +73 -12
- package/lib/config.js +56 -14
- package/lib/dry-run.d.ts +87 -0
- package/lib/dry-run.js +110 -0
- package/lib/engine.js +52 -2
- package/lib/evaluate.js +76 -2
- package/lib/events.d.ts +56 -2
- package/lib/events.js +92 -0
- package/lib/index.js +106 -26
- package/lib/parsers/git.d.ts +43 -0
- package/lib/parsers/git.js +404 -0
- package/lib/parsers/shell-cmds.d.ts +32 -0
- package/lib/parsers/shell-cmds.js +293 -0
- package/lib/preset.d.ts +13 -0
- package/lib/preset.js +17 -0
- package/lib/risk.d.ts +17 -3
- package/lib/risk.js +17 -3
- package/lib/rule-chain.js +13 -22
- package/lib/rule-dims.d.ts +35 -0
- package/lib/rule-dims.js +36 -0
- package/lib/rule.d.ts +36 -1
- package/lib/rule.js +56 -8
- package/lib/rules-view.d.ts +52 -0
- package/lib/rules-view.js +126 -0
- package/lib/runtime.d.ts +121 -7
- package/lib/runtime.js +204 -25
- package/lib/shell.d.ts +14 -0
- package/lib/shell.js +17 -0
- package/package.json +19 -2
package/lib/runtime.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { readFileSync } from 'node:fs';
|
|
12
12
|
import { makeEntry, MemoryAuditMirror } from './audit.js';
|
|
13
13
|
import { resolvePermissiveStrategies } from './config.js';
|
|
14
|
-
import { appendAllowCommand, listAllowCommands, replaceAllowCommands } from './allowlist.js';
|
|
14
|
+
import { appendAllowCommand, listAllowCommands, listAllowFromRulesDoc, replaceAllowCommands } from './allowlist.js';
|
|
15
15
|
import { decide, CONTENT_ARG_KEYS } from './engine.js';
|
|
16
16
|
import { EventLog, saveEventSnapshots } from './events.js';
|
|
17
17
|
import { decideRules } from './evaluate.js';
|
|
@@ -21,13 +21,34 @@ import { ArtifactRegistry } from './path.js';
|
|
|
21
21
|
import { classifyRisk, classifyRiskWith } from './risk.js';
|
|
22
22
|
import { completeViaHost, DEFAULT_HOST_MODEL } from './host-llm.js';
|
|
23
23
|
import { chatCompletion } from './classifier.js';
|
|
24
|
-
import { compileDocument, documentHash, extractPathCandidates, parsePermissionsDocument } from './rule.js';
|
|
24
|
+
import { compileDocument, compileRulesObject, documentHash, extractPathCandidates, parsePermissionsDocument, } from './rule.js';
|
|
25
25
|
import { decomposeShellCommand } from './shell.js';
|
|
26
26
|
import { extractAgentCandidates } from './agent-identity.js';
|
|
27
27
|
import { resolveRuleChain } from './rule-chain.js';
|
|
28
28
|
import { DEFAULT_DENY_KEYWORDS } from './deny-defaults.js';
|
|
29
29
|
import { permissionPresetOf, presetInScope } from './preset.js';
|
|
30
30
|
import { resolveGatePresets } from './config.js';
|
|
31
|
+
/** Locate a rule by its chain-wide index (deny entries first, then allow, then ask). */
|
|
32
|
+
function findRuleByIndex(ruleset, index) {
|
|
33
|
+
return (ruleset.deny.find((entry) => entry.index === index) ??
|
|
34
|
+
ruleset.allow.find((entry) => entry.index === index) ??
|
|
35
|
+
ruleset.ask.find((entry) => entry.index === index));
|
|
36
|
+
}
|
|
37
|
+
/** Which layer of the chain produced a decision, from its stage. */
|
|
38
|
+
function sourceOfStage(stage, reason) {
|
|
39
|
+
switch (stage) {
|
|
40
|
+
case 'hard-deny':
|
|
41
|
+
return 'hard-deny';
|
|
42
|
+
case 'grant':
|
|
43
|
+
return 'grant';
|
|
44
|
+
case 'rule':
|
|
45
|
+
return 'rule';
|
|
46
|
+
case 'classifier':
|
|
47
|
+
return 'classifier';
|
|
48
|
+
default:
|
|
49
|
+
return reason.startsWith('permissive') ? 'permissive' : 'default';
|
|
50
|
+
}
|
|
51
|
+
}
|
|
31
52
|
/**
|
|
32
53
|
* One execution's session event log, or `undefined` when the host exposes none.
|
|
33
54
|
*
|
|
@@ -209,6 +230,12 @@ export class PermGateRuntime {
|
|
|
209
230
|
* frozen events, which would otherwise re-fold on every tool call.
|
|
210
231
|
*/
|
|
211
232
|
presetCache;
|
|
233
|
+
/**
|
|
234
|
+
* Last stand-down announced per session (sessionId → the out-of-scope preset
|
|
235
|
+
* name, `''` when the session records none). Bounded by the session count and
|
|
236
|
+
* only read on the stand-down path — the in-scope path never touches it.
|
|
237
|
+
*/
|
|
238
|
+
standDownAnnounced = new Map();
|
|
212
239
|
learning;
|
|
213
240
|
events;
|
|
214
241
|
/** Learning candidates awaiting human approval + execution (call fingerprint → candidate). */
|
|
@@ -252,6 +279,24 @@ export class PermGateRuntime {
|
|
|
252
279
|
deny: [], allow: [], ask: [],
|
|
253
280
|
caseInsensitivePaths: options.caseInsensitivePaths ?? true,
|
|
254
281
|
};
|
|
282
|
+
// Settings-first (dual-source): a configured `dsh-perm-gate-rules`
|
|
283
|
+
// namespace overrides every file path — the gate then loads with zero
|
|
284
|
+
// filesystem dependency. Bare defaults (nothing configured) fall through
|
|
285
|
+
// to the file sources below. Compile errors propagate: the constructor
|
|
286
|
+
// fails loud, and `reload()` keeps the previous rules on a hot change.
|
|
287
|
+
const settingsDoc = options.readRulesDocument?.();
|
|
288
|
+
if (settingsDoc !== undefined) {
|
|
289
|
+
const hash = documentHash(JSON.stringify(settingsDoc));
|
|
290
|
+
const hit = this.cache.get(hash);
|
|
291
|
+
if (hit !== undefined)
|
|
292
|
+
return hit;
|
|
293
|
+
const compiled = compileRulesObject(settingsDoc, {
|
|
294
|
+
maxGlobStars: 2,
|
|
295
|
+
caseInsensitivePaths: options.caseInsensitivePaths ?? true,
|
|
296
|
+
});
|
|
297
|
+
this.cache.set(hash, compiled);
|
|
298
|
+
return compiled;
|
|
299
|
+
}
|
|
255
300
|
// Chain mode: when searchUp is enabled, use the multi-file chain resolver.
|
|
256
301
|
if (options.searchUp === true) {
|
|
257
302
|
const cwd = options.cwd ?? process.cwd();
|
|
@@ -589,9 +634,38 @@ export class PermGateRuntime {
|
|
|
589
634
|
* `danger-full-access` means "full access without approval prompts", where a
|
|
590
635
|
* forwarded ask can only fail (the approval seam rejects before any answerer
|
|
591
636
|
* runs) and a hard-deny would silently overrule the tier the user chose.
|
|
637
|
+
*
|
|
638
|
+
* P0 is therefore monotonic only WITHIN the gate's scope, not across every
|
|
639
|
+
* preset. {@link announceStandDown} makes that visible instead of silent.
|
|
592
640
|
*/
|
|
593
|
-
gateActive(
|
|
594
|
-
return presetInScope(
|
|
641
|
+
gateActive(preset) {
|
|
642
|
+
return presetInScope(preset, this.gatePresets);
|
|
643
|
+
}
|
|
644
|
+
/**
|
|
645
|
+
* Record one `stand-down` event the first time a session is observed with a
|
|
646
|
+
* preset outside {@link gatePresets}, and again whenever that preset changes.
|
|
647
|
+
*
|
|
648
|
+
* Per-call silence is what makes a stand-down dangerous: the tool call looks
|
|
649
|
+
* exactly like a call the gate inspected and allowed. One event per
|
|
650
|
+
* transition is enough to say otherwise, and keeps the feed (and the
|
|
651
|
+
* `events.jsonl` append) proportional to preset changes rather than to call
|
|
652
|
+
* volume.
|
|
653
|
+
*
|
|
654
|
+
* The event is a NOTICE, not a decision: it carries no allow/deny meaning,
|
|
655
|
+
* and the call it was observed on is still settled entirely by the selected
|
|
656
|
+
* tier. `verdict: 'stand-down'` labels it for the history view.
|
|
657
|
+
*/
|
|
658
|
+
announceStandDown(exec, preset) {
|
|
659
|
+
const sessionId = sessionIdOf(exec);
|
|
660
|
+
const announced = preset ?? '';
|
|
661
|
+
if (this.standDownAnnounced.get(sessionId) === announced)
|
|
662
|
+
return;
|
|
663
|
+
this.standDownAnnounced.set(sessionId, announced);
|
|
664
|
+
const scope = this.gatePresets.includes('*') ? 'every preset' : this.gatePresets.join(', ');
|
|
665
|
+
const named = preset === undefined ? 'no permission/preset recorded' : `"${preset}"`;
|
|
666
|
+
this.recordEvent(exec, 'stand-down', `perm-gate is INACTIVE: the session permission preset is ${named} and the gate scope is ${scope}. `
|
|
667
|
+
+ 'No P0 hard-deny, no rule, no ask and no allow is evaluated for any call in this session — '
|
|
668
|
+
+ 'the selected tier\'s own policy governs. Select a preset in the gate scope to re-arm it.', { verdict: 'stand-down' });
|
|
595
669
|
}
|
|
596
670
|
/**
|
|
597
671
|
* Whether an `ask` produced while the gate is active can actually reach a
|
|
@@ -808,10 +882,16 @@ export class PermGateRuntime {
|
|
|
808
882
|
*/
|
|
809
883
|
decideExecution(exec) {
|
|
810
884
|
// The gate is scoped to its own tier(s): anywhere else it stands down
|
|
811
|
-
// entirely and
|
|
812
|
-
|
|
885
|
+
// entirely and decides nothing (the session's selected tier owns the call).
|
|
886
|
+
// The stand-down is announced ONCE per (session, preset) transition — the
|
|
887
|
+
// call's outcome is unchanged, but the user must be able to see that the
|
|
888
|
+
// gate — including P0 hard-deny — is inactive; a silent stand-down reads as
|
|
889
|
+
// "the gate looked at this and allowed it".
|
|
890
|
+
const preset = this.presetOf(exec);
|
|
891
|
+
if (!this.gateActive(preset)) {
|
|
892
|
+
this.announceStandDown(exec, preset);
|
|
813
893
|
return undefined;
|
|
814
|
-
|
|
894
|
+
}
|
|
815
895
|
const callId = randomId();
|
|
816
896
|
// Preset deny-keyword layer (deny wins over allow): a dangerous-keyword hit
|
|
817
897
|
// vetoes before grants / rules / LLM. Additive deny only — P0 semantics are
|
|
@@ -823,13 +903,9 @@ export class PermGateRuntime {
|
|
|
823
903
|
this.recordEvent(exec, 'deny', reason, { verdict: 'deny-keyword' });
|
|
824
904
|
return { kind: 'deny', reason };
|
|
825
905
|
}
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
return this.grants.decide(tool, args);
|
|
830
|
-
};
|
|
831
|
-
const raw = decide(ctx, { decide: (c) => decideRules(this.ruleset, c) }, grantResolver, this.autoAllowExtra);
|
|
832
|
-
const decision = this.applyPermissive(raw);
|
|
906
|
+
// The same chain `explainCall` reports, evaluated once here so the two views
|
|
907
|
+
// cannot drift; this caller adds the side effects and the session layers.
|
|
908
|
+
const decision = this.applyPermissive(this.rawPolicy(exec));
|
|
833
909
|
// Deterministic cleanup pre-screen: if a deletion command targets a
|
|
834
910
|
// regenerable/temporary artifact inside the workspace, allow it directly.
|
|
835
911
|
// This runs BEFORE the ask/passthrough logic so it works even under
|
|
@@ -898,6 +974,83 @@ export class PermGateRuntime {
|
|
|
898
974
|
this.clearCall(exec, decision.reason, source, source);
|
|
899
975
|
return undefined;
|
|
900
976
|
}
|
|
977
|
+
/**
|
|
978
|
+
* The **pure policy** outcome for one call: the same chain
|
|
979
|
+
* {@link decideExecution} runs, with every side effect and every session-state
|
|
980
|
+
* layer removed.
|
|
981
|
+
*
|
|
982
|
+
* Side effects removed — no audit entry, no event, no ask tracking, no call
|
|
983
|
+
* clearance, no learning. A rule-test panel must be able to run on every
|
|
984
|
+
* keystroke without writing to the live decision feed.
|
|
985
|
+
*
|
|
986
|
+
* Session-state layers removed — the preset stand-down and the
|
|
987
|
+
* `approval: never` ask degradation both describe a session a session-less
|
|
988
|
+
* caller does not have. Including them is not harmless: the degradation turns
|
|
989
|
+
* every "the rules would ask a human" into a reported **allow**, which is the
|
|
990
|
+
* opposite of the truth for exactly the rules someone opens the panel to
|
|
991
|
+
* review (measured on the live host: `shell ls -la` reported `allow` while the
|
|
992
|
+
* rule layer said `ask`).
|
|
993
|
+
*
|
|
994
|
+
* `reason` is never collapsed: "allow" has causes worth naming (a rule, a
|
|
995
|
+
* grant, cleanup-safety, the permissive default).
|
|
996
|
+
*/
|
|
997
|
+
explainCall(exec) {
|
|
998
|
+
const keyword = this.denyKeywordHit(exec);
|
|
999
|
+
if (keyword !== undefined) {
|
|
1000
|
+
return {
|
|
1001
|
+
action: 'deny',
|
|
1002
|
+
reason: `deny-keyword: matches preset blacklist entry "${keyword}"`,
|
|
1003
|
+
source: 'deny-keyword',
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
const decision = this.applyPermissive(this.rawPolicy(exec));
|
|
1007
|
+
if (decision.action === 'ask') {
|
|
1008
|
+
const cleanupSafe = isCleanupSafe(exec);
|
|
1009
|
+
if (cleanupSafe !== undefined) {
|
|
1010
|
+
return { action: 'allow', reason: `cleanup-safe: ${cleanupSafe}`, source: 'cleanup-safe' };
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
return {
|
|
1014
|
+
action: decision.action,
|
|
1015
|
+
reason: decision.reason,
|
|
1016
|
+
source: sourceOfStage(decision.stage, decision.reason),
|
|
1017
|
+
...(decision.ruleIndex !== undefined ? { ruleIndex: decision.ruleIndex } : {}),
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
/**
|
|
1021
|
+
* P0 hard-deny → P1 session grant → P2 rules → P4 ask, as a value. Shared by
|
|
1022
|
+
* the host-facing decision and {@link explainCall} so the two cannot drift.
|
|
1023
|
+
*/
|
|
1024
|
+
rawPolicy(exec) {
|
|
1025
|
+
const grantResolver = (tool, args) => {
|
|
1026
|
+
if (exec.parentAuthorized === false)
|
|
1027
|
+
return 'no-match';
|
|
1028
|
+
return this.grants.decide(tool, args);
|
|
1029
|
+
};
|
|
1030
|
+
return decide(this.ctxFor(exec), { decide: (c) => decideRules(this.ruleset, c) }, grantResolver, this.autoAllowExtra);
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Rule-layer explanation for one would-be call: what the `permissions` chain
|
|
1034
|
+
* decides on its own, before P0 hard-deny, P1 session grants, the P3
|
|
1035
|
+
* classifier and the P4 ask ever get a say.
|
|
1036
|
+
*
|
|
1037
|
+
* This is the read-only half of a dry-run. It appends nothing to the audit
|
|
1038
|
+
* mirror, records no event, mints no grant and touches no learning store, so a
|
|
1039
|
+
* "rule test" panel may call it on every keystroke without leaving a trace in
|
|
1040
|
+
* the decision feed. It answers a narrower question than
|
|
1041
|
+
* {@link decideExecution} and says so: `ruleIndex` identifies a rule in the
|
|
1042
|
+
* chain, which only this layer can attribute.
|
|
1043
|
+
*/
|
|
1044
|
+
explainRules(exec) {
|
|
1045
|
+
const decision = decideRules(this.ruleset, this.ctxFor(exec));
|
|
1046
|
+
return {
|
|
1047
|
+
action: decision.action,
|
|
1048
|
+
reason: decision.reason,
|
|
1049
|
+
ruleIndex: decision.ruleIndex,
|
|
1050
|
+
rule: decision.ruleIndex === undefined ? undefined : findRuleByIndex(this.ruleset, decision.ruleIndex),
|
|
1051
|
+
defaultAction: this.ruleset.defaultAction,
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
901
1054
|
/**
|
|
902
1055
|
* Apply the Permissive tier to a raw decision. Hard-deny and minted-grant
|
|
903
1056
|
* outcomes are never touched (fail-closed). `alwaysConfirm` escalates a
|
|
@@ -1004,13 +1157,22 @@ export class PermGateRuntime {
|
|
|
1004
1157
|
if (risk.kind === 'risky') {
|
|
1005
1158
|
if (risk.category !== 'neutral') {
|
|
1006
1159
|
// Hard category (deletion / credential / remote / system / bulk) or an
|
|
1007
|
-
// off-protocol output graded hard:
|
|
1008
|
-
//
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1160
|
+
// off-protocol output graded hard: KEEP THE ASK.
|
|
1161
|
+
//
|
|
1162
|
+
// Denying here would make a probabilistic verdict the source of an
|
|
1163
|
+
// unappealable block: the gate would be "sure enough" to refuse on the
|
|
1164
|
+
// model's word alone, with no popup to appeal to and no grant to retry
|
|
1165
|
+
// with — measured live on a benign `git commit -F …` the grader called
|
|
1166
|
+
// `remote`. Deny stays with the deterministic layers (P0 hard-deny, the
|
|
1167
|
+
// deny-keyword blacklist, explicit `deny:` rules); everything the
|
|
1168
|
+
// classifier merely *suspects* is negotiated with the human instead.
|
|
1169
|
+
//
|
|
1170
|
+
// Hard categories stay distinct from `neutral` in one way that matters:
|
|
1171
|
+
// they are NEVER learnable, so repeated confirmations cannot sediment
|
|
1172
|
+
// them into an auto-allow.
|
|
1173
|
+
const hardReason = `${decision.reason} [llm-assist risky:${risk.category} → ask]`;
|
|
1174
|
+
this.trackAsk(exec, hardReason, undefined);
|
|
1175
|
+
return { kind: 'ask', reason: hardReason };
|
|
1014
1176
|
}
|
|
1015
1177
|
const learning = this.liveRiskLearning();
|
|
1016
1178
|
const fp = operationFingerprint(exec.name, exec.arguments ?? {}, exec.commandText);
|
|
@@ -1249,27 +1411,44 @@ export class PermGateRuntime {
|
|
|
1249
1411
|
}
|
|
1250
1412
|
/**
|
|
1251
1413
|
* Grant "allow every occurrence of this command" (the always-confirm panel's
|
|
1252
|
-
* second extended allow button): persist the command into the rules
|
|
1253
|
-
*
|
|
1414
|
+
* second extended allow button): persist the command into the rules' `allow`
|
|
1415
|
+
* whitelist and reload. Prefers the settings-backed writer (the namespace's
|
|
1416
|
+
* watch triggers the reload once the write lands); falls back to the rules
|
|
1417
|
+
* file when no settings scope is wired. Returns the write verdict.
|
|
1254
1418
|
*/
|
|
1255
1419
|
approveAllowEverywhere(commandWord, reason = 'permissive allow-everywhere') {
|
|
1420
|
+
const writer = this.options.allowlistWriter;
|
|
1421
|
+
if (writer !== undefined && writer.append(commandWord, reason))
|
|
1422
|
+
return true;
|
|
1256
1423
|
if (!this.options.rulesFile)
|
|
1257
1424
|
return false;
|
|
1258
1425
|
if (!appendAllowCommand(this.options.rulesFile, commandWord, reason))
|
|
1259
1426
|
return false;
|
|
1260
1427
|
return this.reload();
|
|
1261
1428
|
}
|
|
1262
|
-
/**
|
|
1429
|
+
/**
|
|
1430
|
+
* Read-only view of the current allow-list command patterns (whitelist).
|
|
1431
|
+
* Reads the settings document when the namespace is configured, else the
|
|
1432
|
+
* rules file.
|
|
1433
|
+
*/
|
|
1263
1434
|
allowlist() {
|
|
1435
|
+
const doc = this.options.readRulesDocument?.();
|
|
1436
|
+
if (doc !== undefined)
|
|
1437
|
+
return listAllowFromRulesDoc(doc);
|
|
1264
1438
|
if (!this.options.rulesFile)
|
|
1265
1439
|
return [];
|
|
1266
1440
|
return listAllowCommands(this.options.rulesFile);
|
|
1267
1441
|
}
|
|
1268
1442
|
/**
|
|
1269
1443
|
* Replace the whitelist with exactly the given command patterns and reload.
|
|
1270
|
-
*
|
|
1444
|
+
* Prefers the settings-backed writer; falls back to the rules file when no
|
|
1445
|
+
* settings scope is wired. Returns the write verdict (false when neither
|
|
1446
|
+
* sink is available or the write failed).
|
|
1271
1447
|
*/
|
|
1272
1448
|
setAllowlist(patterns) {
|
|
1449
|
+
const writer = this.options.allowlistWriter;
|
|
1450
|
+
if (writer !== undefined && writer.replace(patterns, 'permissive allowlist'))
|
|
1451
|
+
return true;
|
|
1273
1452
|
if (!this.options.rulesFile)
|
|
1274
1453
|
return false;
|
|
1275
1454
|
if (!replaceAllowCommands(this.options.rulesFile, patterns))
|
package/lib/shell.d.ts
CHANGED
|
@@ -19,6 +19,20 @@ export interface SimpleCommand {
|
|
|
19
19
|
readonly redirects: readonly string[];
|
|
20
20
|
}
|
|
21
21
|
export type CmdFlag = 'recursive' | 'force';
|
|
22
|
+
/**
|
|
23
|
+
* Reconstruct a simple command's full argv as one string.
|
|
24
|
+
*
|
|
25
|
+
* `SimpleCommand` splits the command into `command` + `args`, so any consumer
|
|
26
|
+
* that matches on a *whole command line* (the `argv.pipeline` dimension, the
|
|
27
|
+
* `branch` dimension's git parser) needs the parts joined back together —
|
|
28
|
+
* feeding only `command` loses every subcommand and flag (`git push --force
|
|
29
|
+
* origin main` degrades to `git`, which parses as "not a git command").
|
|
30
|
+
*
|
|
31
|
+
* Redirect targets are appended so a pattern naming the redirect target can
|
|
32
|
+
* match, and tokens containing whitespace are quoted so the reconstruction
|
|
33
|
+
* stays tokenizable.
|
|
34
|
+
*/
|
|
35
|
+
export declare function commandArgv(cmd: SimpleCommand): string;
|
|
22
36
|
/**
|
|
23
37
|
* Tokenize a shell string into tokens, honoring single/double quotes and a few
|
|
24
38
|
* common escapes. Missing/EOF quotes throw so the caller can route an
|
package/lib/shell.js
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reconstruct a simple command's full argv as one string.
|
|
3
|
+
*
|
|
4
|
+
* `SimpleCommand` splits the command into `command` + `args`, so any consumer
|
|
5
|
+
* that matches on a *whole command line* (the `argv.pipeline` dimension, the
|
|
6
|
+
* `branch` dimension's git parser) needs the parts joined back together —
|
|
7
|
+
* feeding only `command` loses every subcommand and flag (`git push --force
|
|
8
|
+
* origin main` degrades to `git`, which parses as "not a git command").
|
|
9
|
+
*
|
|
10
|
+
* Redirect targets are appended so a pattern naming the redirect target can
|
|
11
|
+
* match, and tokens containing whitespace are quoted so the reconstruction
|
|
12
|
+
* stays tokenizable.
|
|
13
|
+
*/
|
|
14
|
+
export function commandArgv(cmd) {
|
|
15
|
+
const quote = (token) => (/\s/.test(token) ? JSON.stringify(token) : token);
|
|
16
|
+
return [cmd.command, ...cmd.args, ...cmd.redirects].map(quote).join(' ');
|
|
17
|
+
}
|
|
1
18
|
const WRAPPER_WORDS = new Set(['env', 'sudo', 'command', 'nohup', 'xargs']);
|
|
2
19
|
const COMMAND_WORDS = new Set(['sh', 'bash', 'zsh', 'dash', 'busybox', 'pwsh', 'powershell', 'cmd']);
|
|
3
20
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-perm-gate",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"registry": "https://registry.npmjs.org",
|
|
6
|
+
"access": "public",
|
|
7
|
+
"tag": "dsh-0.1.2"
|
|
8
|
+
},
|
|
4
9
|
"description": "DSH permission-gate for the DeepSeek Harness 0.1.2+ line: a single self-sufficient, deterministic-first, fail-closed gate covering P0 hard-deny -> P1 session grant -> P2 static rule (allow/deny) chain -> P3 optional LLM semantic classifier -> P4 ask, with command whitelist/blacklist.",
|
|
5
10
|
"type": "module",
|
|
6
11
|
"files": [
|
|
@@ -9,6 +14,7 @@
|
|
|
9
14
|
"cordis.patch.yml",
|
|
10
15
|
"dsh.plugin.json",
|
|
11
16
|
"assets",
|
|
17
|
+
"docs",
|
|
12
18
|
"examples",
|
|
13
19
|
"README.md",
|
|
14
20
|
"README.zh.md",
|
|
@@ -64,7 +70,17 @@
|
|
|
64
70
|
"dsh": ">=0.1.2-alpha.1 <0.2.0-0"
|
|
65
71
|
},
|
|
66
72
|
"peerDependencies": {
|
|
67
|
-
"@deepseek-ai/cordis": "^4.0.1"
|
|
73
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
74
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.2-alpha.1 <0.2.0-0",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.2-alpha.1 <0.2.0-0",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.2-alpha.1 <0.2.0-0",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.2-alpha.1 <0.2.0-0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"@deepseek-ai/dsh-client-locale": { "optional": true },
|
|
81
|
+
"@deepseek-ai/dsh-client-ui-settings": { "optional": true },
|
|
82
|
+
"@deepseek-ai/dsh-client-ui-renderer": { "optional": true },
|
|
83
|
+
"@deepseek-ai/dsh-client-ui-conversation": { "optional": true }
|
|
68
84
|
},
|
|
69
85
|
"dependencies": {
|
|
70
86
|
"@deepseek-ai/schemastery": "^3.18.0",
|
|
@@ -97,6 +113,7 @@
|
|
|
97
113
|
"patch:glyph:check": "node scripts/patch-permission-glyph.mjs --check",
|
|
98
114
|
"verify:line": "node scripts/verify-line.mjs new",
|
|
99
115
|
"verify:lines": "node scripts/verify-line.mjs both",
|
|
116
|
+
"check:line": "node scripts/sync-0.1.5-line.mjs check",
|
|
100
117
|
"release:latest": "npm publish --tag latest",
|
|
101
118
|
"release:2x": "npm publish --tag 2.x",
|
|
102
119
|
"release:legacy": "npm publish --tag legacy"
|