@wrongstack/core 0.306.2 → 0.306.4
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/coordination/index.js +184 -86
- package/dist/coordination/mail-tools.d.ts +1 -1
- package/dist/coordination/mailbox-project-server.js +38 -16
- package/dist/coordination/sqlite-mailbox.d.ts +1 -0
- package/dist/core/conversation-state.d.ts +1 -2
- package/dist/core/fallback-model.d.ts +19 -1
- package/dist/core/fallback-profile-manager.d.ts +21 -3
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.js +152 -46
- package/dist/defaults/index.js +347 -139
- package/dist/execution/auto-compaction-middleware.d.ts +64 -0
- package/dist/execution/compaction-core.d.ts +4 -0
- package/dist/execution/index.d.ts +1 -1
- package/dist/execution/index.js +234 -91
- package/dist/execution/retry-policy.d.ts +27 -0
- package/dist/hq/index.js +50 -15
- package/dist/index.d.ts +1 -1
- package/dist/index.js +642 -184
- package/dist/infrastructure/index.js +21 -0
- package/dist/plugin/index.js +195 -19
- package/dist/security/index.js +52 -16
- package/dist/security/kanban-boundary.d.ts +3 -1
- package/dist/session-catalog/index.js +50 -15
- package/dist/session-catalog/project-server.js +50 -15
- package/dist/storage/cloud-config-sync/sanitize.d.ts +18 -0
- package/dist/storage/cloud-config-sync.d.ts +1 -1
- package/dist/storage/index.js +326 -71
- package/dist/storage/provider-config-watcher.d.ts +9 -0
- package/dist/storage/session-store/strict-empty-check.d.ts +7 -0
- package/dist/storage/session-store.d.ts +1 -0
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +83 -22
- package/dist/types/blocks.d.ts +9 -0
- package/dist/types/config/root.d.ts +14 -0
- package/dist/types/session.d.ts +7 -0
- package/instructions/agents/code-reviewer.md +3 -0
- package/instructions/coordination/subagent-baseline.md +10 -1
- package/instructions/system-lite.md +8 -5
- package/instructions/system-pro.md +26 -0
- package/instructions/system.md +21 -0
- package/package.json +3 -3
|
@@ -3456,6 +3456,10 @@ var IN_PROJECT_ALLOWED_KEYS = /* @__PURE__ */ new Set([
|
|
|
3456
3456
|
"fallbackModels",
|
|
3457
3457
|
"fallbackBridge",
|
|
3458
3458
|
"fallbackProfiles",
|
|
3459
|
+
// The profile SELECTOR. No broader than its siblings: a repo that can write
|
|
3460
|
+
// `fallbackModels` and `fallbackProfiles` already controls the chain outright,
|
|
3461
|
+
// and this one can only name a profile the user already defined.
|
|
3462
|
+
"fallbackProfile",
|
|
3459
3463
|
"favoriteModels",
|
|
3460
3464
|
"favoriteModelsOnly",
|
|
3461
3465
|
"modelAvailabilitySchedule",
|
|
@@ -3503,6 +3507,10 @@ var KNOWN_DENIED_IN_PROJECT = [
|
|
|
3503
3507
|
{
|
|
3504
3508
|
key: "git",
|
|
3505
3509
|
reason: "Carries git.identity (GIT_AUTHOR_*/GIT_COMMITTER_* injection): a repo-committed config could spoof the author identity written into the victim's commit history (impersonation)."
|
|
3510
|
+
},
|
|
3511
|
+
{
|
|
3512
|
+
key: "fallbackMaxLastResortCandidates",
|
|
3513
|
+
reason: "Bounds how many of the user's OTHER configured providers may be swept in as last-resort failover. Setting it to 0 from a repo-committed config would silently strip that depth during an outage. It was already stripped in practice (absent from the allow-list) but was missing from the key registry, so this gate never checked it."
|
|
3506
3514
|
}
|
|
3507
3515
|
];
|
|
3508
3516
|
var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -3523,11 +3531,13 @@ var KNOWN_CONFIG_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
3523
3531
|
"fallbackModels",
|
|
3524
3532
|
"fallbackBridge",
|
|
3525
3533
|
"fallbackProfiles",
|
|
3534
|
+
"fallbackProfile",
|
|
3526
3535
|
"favoriteModels",
|
|
3527
3536
|
"favoriteModelsOnly",
|
|
3528
3537
|
"modelAvailabilitySchedule",
|
|
3529
3538
|
"fallbackAuto",
|
|
3530
3539
|
"fallbackStickiness",
|
|
3540
|
+
"fallbackMaxLastResortCandidates",
|
|
3531
3541
|
"hooks",
|
|
3532
3542
|
"plugins",
|
|
3533
3543
|
"pluginManager",
|
|
@@ -3613,6 +3623,17 @@ var IN_PROJECT_DENIED_PATHS = [
|
|
|
3613
3623
|
// operator owns, not the checked-out repository.
|
|
3614
3624
|
path: "tools.kanbanGovernance",
|
|
3615
3625
|
reason: "Repo-committed config could disable a Kanban governance gate the operator switched on, letting product mutations run outside any managed card."
|
|
3626
|
+
},
|
|
3627
|
+
{
|
|
3628
|
+
// The bridge spawn path resolves the CLI entry by walking UP from the
|
|
3629
|
+
// project root, so a repo that ships its own `packages/cli/dist/index.js`
|
|
3630
|
+
// gets that file spawned with `process.execPath` on WebUI boot — no
|
|
3631
|
+
// prompt, no banner. Turning the feature on is therefore equivalent to
|
|
3632
|
+
// arbitrary code execution for a hostile checkout, which makes this an
|
|
3633
|
+
// operator-owned switch and never a repo-owned one.
|
|
3634
|
+
// See discover-mailbox-bridge.ts:findWorkspaceCliEntry.
|
|
3635
|
+
path: "features.mailboxBridge",
|
|
3636
|
+
reason: "Enables the mailbox bridge, whose CLI-entry resolution walks up from the project root \u2014 a repo-supplied packages/cli/dist/index.js would be spawned on WebUI boot."
|
|
3616
3637
|
}
|
|
3617
3638
|
];
|
|
3618
3639
|
function deleteNestedPath(target, path13) {
|
package/dist/plugin/index.js
CHANGED
|
@@ -2346,6 +2346,12 @@ function parseModelRef(ref) {
|
|
|
2346
2346
|
function hasText(value) {
|
|
2347
2347
|
return typeof value === "string" && value.trim().length > 0;
|
|
2348
2348
|
}
|
|
2349
|
+
function asRefList(value) {
|
|
2350
|
+
return Array.isArray(value) ? value : void 0;
|
|
2351
|
+
}
|
|
2352
|
+
function asProfileName(value) {
|
|
2353
|
+
return hasText(value) ? value : void 0;
|
|
2354
|
+
}
|
|
2349
2355
|
function providerHasKey(entry) {
|
|
2350
2356
|
if (!entry) return false;
|
|
2351
2357
|
if (hasText(entry.apiKey)) return true;
|
|
@@ -2356,7 +2362,7 @@ function providerHasKey(entry) {
|
|
|
2356
2362
|
}
|
|
2357
2363
|
function visibleProviderModels(config, providerId, providerModels) {
|
|
2358
2364
|
const entry = config.providers?.[providerId];
|
|
2359
|
-
return entry?.models
|
|
2365
|
+
return Array.isArray(entry?.models) ? [...entry.models] : providerModels;
|
|
2360
2366
|
}
|
|
2361
2367
|
function buildProfiles(config) {
|
|
2362
2368
|
const entries = /* @__PURE__ */ new Map();
|
|
@@ -2393,13 +2399,34 @@ var FallbackProfileManager = class {
|
|
|
2393
2399
|
listProfiles() {
|
|
2394
2400
|
return Object.freeze([...this.profiles.keys()]);
|
|
2395
2401
|
}
|
|
2402
|
+
/**
|
|
2403
|
+
* The profile the session has selected (`config.fallbackProfile`, set by
|
|
2404
|
+
* `/fallback profile use <name>`), or undefined when none is selected or the
|
|
2405
|
+
* name no longer resolves to a defined profile.
|
|
2406
|
+
*
|
|
2407
|
+
* Consulted by every resolution entry point when the caller does not name a
|
|
2408
|
+
* profile itself. Without this the leader — which passes no profile — could
|
|
2409
|
+
* never use a named profile at all: `config.fallbackProfiles` was reachable
|
|
2410
|
+
* only by copying a chain into `fallbackModels`.
|
|
2411
|
+
*/
|
|
2412
|
+
activeProfileName() {
|
|
2413
|
+
const name = asProfileName(this.config.fallbackProfile);
|
|
2414
|
+
return name && this.profiles.has(name) ? name : void 0;
|
|
2415
|
+
}
|
|
2396
2416
|
// ── Resolution ─────────────────────────────────────────────────────────
|
|
2397
2417
|
/**
|
|
2398
2418
|
* Resolve a named fallback profile to a validated, provider-filtered chain.
|
|
2399
2419
|
*
|
|
2400
|
-
* Returns an empty chain when
|
|
2401
|
-
*
|
|
2402
|
-
*
|
|
2420
|
+
* Returns an empty chain when the profile doesn't exist, or when every entry
|
|
2421
|
+
* is excluded, quarantined, or blacked out.
|
|
2422
|
+
*
|
|
2423
|
+
* Filtering is intentionally identical to {@link resolveRefs} (the explicit
|
|
2424
|
+
* `fallbackModels` path): the self-exclusion, the runtime status tracker,
|
|
2425
|
+
* and the availability calendar — nothing else. Anything a named profile
|
|
2426
|
+
* drops, an explicit chain drops too, and vice versa. Profiles used to apply
|
|
2427
|
+
* two extra filters (provider "usability" and the `providers[].models`
|
|
2428
|
+
* snapshot) that the explicit path did not, which silently rerouted roles
|
|
2429
|
+
* pinned to a profile onto a different model than the one configured.
|
|
2403
2430
|
*
|
|
2404
2431
|
* @param name - Profile name from config.fallbackProfiles.
|
|
2405
2432
|
* @param defaultProvider - Used when an entry has no explicit provider.
|
|
@@ -2420,13 +2447,9 @@ var FallbackProfileManager = class {
|
|
|
2420
2447
|
if (seen.has(key)) continue;
|
|
2421
2448
|
seen.add(key);
|
|
2422
2449
|
if (excludeKey && key === excludeKey) continue;
|
|
2423
|
-
const health = this.checkProvider(providerId);
|
|
2424
|
-
if (!health.usable) continue;
|
|
2425
2450
|
if (this.statusTracker && !this.statusTracker.isAvailable(providerId, parsed.model)) continue;
|
|
2426
2451
|
if (!evaluateModelCalendar(this.config.modelAvailabilitySchedule, providerId, parsed.model).allowed)
|
|
2427
2452
|
continue;
|
|
2428
|
-
const allowedModels = this.config.providers?.[providerId]?.models;
|
|
2429
|
-
if (allowedModels && !allowedModels.includes(parsed.model)) continue;
|
|
2430
2453
|
resolved.push({
|
|
2431
2454
|
providerId,
|
|
2432
2455
|
model: parsed.model,
|
|
@@ -2444,12 +2467,14 @@ var FallbackProfileManager = class {
|
|
|
2444
2467
|
resolveEffective(opts = {}) {
|
|
2445
2468
|
const bridge = this.resolveBridge(opts.exclude);
|
|
2446
2469
|
let selected = FREEZER_EMPTY;
|
|
2447
|
-
|
|
2448
|
-
|
|
2470
|
+
const explicitRefs = asRefList(opts.fallbackModels);
|
|
2471
|
+
const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
|
|
2472
|
+
if (explicitRefs && explicitRefs.length > 0) {
|
|
2473
|
+
const resolved = this.resolveRefs(explicitRefs, opts.exclude);
|
|
2449
2474
|
if (resolved.length > 0) selected = resolved;
|
|
2450
2475
|
}
|
|
2451
|
-
if (selected.length === 0 &&
|
|
2452
|
-
const resolved = this.resolve(
|
|
2476
|
+
if (selected.length === 0 && profileName) {
|
|
2477
|
+
const resolved = this.resolve(profileName, { exclude: opts.exclude });
|
|
2453
2478
|
if (resolved.length > 0) selected = resolved;
|
|
2454
2479
|
}
|
|
2455
2480
|
if (selected.length === 0 && opts.fallbackAuto !== false) {
|
|
@@ -2525,13 +2550,14 @@ var FallbackProfileManager = class {
|
|
|
2525
2550
|
};
|
|
2526
2551
|
const configFallbackAuto = this.config.fallbackAuto;
|
|
2527
2552
|
const effectiveFallbackAuto = configFallbackAuto !== void 0 && configFallbackAuto !== null ? configFallbackAuto : !opts.closedWorld;
|
|
2528
|
-
const explicitRefs = opts.fallbackModels ?? this.config.fallbackModels;
|
|
2553
|
+
const explicitRefs = asRefList(opts.fallbackModels) ?? asRefList(this.config.fallbackModels);
|
|
2554
|
+
const profileName = asProfileName(opts.fallbackProfile) ?? this.activeProfileName();
|
|
2529
2555
|
const explicitUsable = explicitRefs !== void 0 && explicitRefs.length > 0 && this.resolveRefs(explicitRefs, current).length > 0;
|
|
2530
|
-
const profileUsable =
|
|
2556
|
+
const profileUsable = profileName !== void 0 && this.hasProfile(profileName) && this.resolve(profileName, { exclude: current }).length > 0;
|
|
2531
2557
|
const fromExplicitSource = explicitUsable || profileUsable;
|
|
2532
|
-
const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) :
|
|
2558
|
+
const selectedChain = opts.closedWorld ? explicitRefs && explicitRefs.length > 0 ? this.resolveRefs(explicitRefs, current) : profileName ? this.resolve(profileName, { exclude: current }) : FREEZER_EMPTY : this.resolveEffective({
|
|
2533
2559
|
fallbackModels: explicitRefs,
|
|
2534
|
-
fallbackProfile:
|
|
2560
|
+
fallbackProfile: profileName,
|
|
2535
2561
|
fallbackAuto: effectiveFallbackAuto,
|
|
2536
2562
|
exclude: current
|
|
2537
2563
|
});
|
|
@@ -2548,7 +2574,7 @@ var FallbackProfileManager = class {
|
|
|
2548
2574
|
});
|
|
2549
2575
|
}
|
|
2550
2576
|
candidates.push(...selectedChain);
|
|
2551
|
-
if (!fromExplicitSource && effectiveFallbackAuto &&
|
|
2577
|
+
if (!fromExplicitSource && effectiveFallbackAuto && profileName !== "default") {
|
|
2552
2578
|
candidates.push(...this.resolve("default", { exclude: current }));
|
|
2553
2579
|
}
|
|
2554
2580
|
if (!fromExplicitSource && effectiveFallbackAuto) {
|
|
@@ -2626,7 +2652,7 @@ var FallbackProfileManager = class {
|
|
|
2626
2652
|
const leaderModel = this.config.model;
|
|
2627
2653
|
const providers = this.config.providers ?? {};
|
|
2628
2654
|
const favoriteSet = new Set(
|
|
2629
|
-
(this.config.favoriteModels ?? []).map((ref) => {
|
|
2655
|
+
(asRefList(this.config.favoriteModels) ?? []).map((ref) => {
|
|
2630
2656
|
const p = parseModelRef(ref);
|
|
2631
2657
|
return `${p.provider ?? leaderProvider}/${p.model}`;
|
|
2632
2658
|
})
|
|
@@ -13703,6 +13729,156 @@ var CLOUD_SYNC_CONTRACT = {
|
|
|
13703
13729
|
"extensions.plugins": EXTENSIONS_PLUGINS_TREE
|
|
13704
13730
|
};
|
|
13705
13731
|
var CLOUD_SYNC_NAMESPACES = Object.keys(CLOUD_SYNC_CONTRACT);
|
|
13732
|
+
var INBOUND_DENIED_PATHS = [
|
|
13733
|
+
// ── Code execution ──────────────────────────────────────────────────────
|
|
13734
|
+
{
|
|
13735
|
+
namespace: "mcp.servers",
|
|
13736
|
+
path: "mcpServers.*.command",
|
|
13737
|
+
reason: "Executable spawned for a stdio MCP server."
|
|
13738
|
+
},
|
|
13739
|
+
{
|
|
13740
|
+
namespace: "mcp.servers",
|
|
13741
|
+
path: "mcpServers.*.args",
|
|
13742
|
+
reason: "Argv for that executable."
|
|
13743
|
+
},
|
|
13744
|
+
{
|
|
13745
|
+
namespace: "mcp.servers",
|
|
13746
|
+
path: "mcpServers.*.transport",
|
|
13747
|
+
reason: "Switching transport to stdio selects the spawning code path."
|
|
13748
|
+
},
|
|
13749
|
+
{
|
|
13750
|
+
namespace: "extensions.plugins",
|
|
13751
|
+
path: "plugins",
|
|
13752
|
+
reason: "Plugin list is resolved and `await import`ed."
|
|
13753
|
+
},
|
|
13754
|
+
// `extensions` is deliberately NOT denied: it is per-plugin settings read via
|
|
13755
|
+
// `ConfigStore.getExtension(name)`, not a loader list, so it grants no import.
|
|
13756
|
+
// Syncing it is the feature (`extensions.telegram.notifyChatId` and friends).
|
|
13757
|
+
// Residual risk accepted: a plugin whose own settings include a URL can have
|
|
13758
|
+
// that URL rewritten by the portal. Constrain that in the plugin's
|
|
13759
|
+
// `configSchema`, which is where the loader validates it.
|
|
13760
|
+
// ── Credential redirection / exfiltration ───────────────────────────────
|
|
13761
|
+
{
|
|
13762
|
+
namespace: "providers.catalog",
|
|
13763
|
+
path: "providers.*.baseUrl",
|
|
13764
|
+
reason: "Repoints the provider endpoint; reinjectLocalSecrets keeps the local apiKey, so the real key follows the redirect."
|
|
13765
|
+
},
|
|
13766
|
+
{
|
|
13767
|
+
namespace: "providers.catalog",
|
|
13768
|
+
path: "providers.*.envVars",
|
|
13769
|
+
reason: "Chooses which environment variable is read for the key."
|
|
13770
|
+
},
|
|
13771
|
+
{
|
|
13772
|
+
namespace: "providers.catalog",
|
|
13773
|
+
path: "providers.*.activeKey",
|
|
13774
|
+
reason: "Selects which stored key is sent."
|
|
13775
|
+
},
|
|
13776
|
+
{
|
|
13777
|
+
namespace: "mcp.servers",
|
|
13778
|
+
path: "mcpServers.*.url",
|
|
13779
|
+
reason: "Remote MCP endpoint; receives whatever the transport carries."
|
|
13780
|
+
},
|
|
13781
|
+
{
|
|
13782
|
+
namespace: "mcp.servers",
|
|
13783
|
+
path: "mcpServers.*.envVars",
|
|
13784
|
+
reason: "Names of environment variables forwarded to the server process."
|
|
13785
|
+
},
|
|
13786
|
+
// ── Operator-owned safety switches ──────────────────────────────────────
|
|
13787
|
+
{
|
|
13788
|
+
namespace: "mcp.servers",
|
|
13789
|
+
path: "mcpServers.*.permission",
|
|
13790
|
+
reason: "Approval requirement for that server\u2019s tools."
|
|
13791
|
+
},
|
|
13792
|
+
{ namespace: "core.runtime", path: "yolo", reason: "Disables every permission prompt." },
|
|
13793
|
+
{
|
|
13794
|
+
namespace: "core.runtime",
|
|
13795
|
+
path: "features.allowOutsideProjectRoot",
|
|
13796
|
+
reason: "Short-circuits project-root containment and the symlink realpath check."
|
|
13797
|
+
},
|
|
13798
|
+
{
|
|
13799
|
+
namespace: "core.runtime",
|
|
13800
|
+
path: "tools.restrictToProjectRoot",
|
|
13801
|
+
reason: "The other half of the filesystem confinement switch."
|
|
13802
|
+
},
|
|
13803
|
+
{
|
|
13804
|
+
namespace: "core.runtime",
|
|
13805
|
+
path: "features.developerMode",
|
|
13806
|
+
reason: "Loosens guardrails; an operator opt-in, not a synced preference."
|
|
13807
|
+
},
|
|
13808
|
+
{
|
|
13809
|
+
namespace: "core.runtime",
|
|
13810
|
+
path: "tools.disabledTools",
|
|
13811
|
+
reason: "Could re-enable a tool the operator deliberately switched off."
|
|
13812
|
+
},
|
|
13813
|
+
{
|
|
13814
|
+
namespace: "ui.preferences",
|
|
13815
|
+
path: "autonomy.defaultMode",
|
|
13816
|
+
reason: "Autonomy is user-owned, never remote-owned."
|
|
13817
|
+
},
|
|
13818
|
+
{
|
|
13819
|
+
namespace: "ui.preferences",
|
|
13820
|
+
path: "autonomy.yolo",
|
|
13821
|
+
reason: "Alias for the denied top-level `yolo`, and it wins over the user setting."
|
|
13822
|
+
},
|
|
13823
|
+
{
|
|
13824
|
+
namespace: "ui.preferences",
|
|
13825
|
+
path: "launch.autonomy",
|
|
13826
|
+
reason: "Launch-time autonomy mode; same user-owned boundary."
|
|
13827
|
+
},
|
|
13828
|
+
{
|
|
13829
|
+
namespace: "models.routing",
|
|
13830
|
+
path: "brain.mode",
|
|
13831
|
+
reason: "Selects the policy/LLM/human decision ladder."
|
|
13832
|
+
},
|
|
13833
|
+
{
|
|
13834
|
+
namespace: "models.routing",
|
|
13835
|
+
path: "brain.maxAutoRisk",
|
|
13836
|
+
reason: "The risk ceiling below which actions proceed without asking."
|
|
13837
|
+
}
|
|
13838
|
+
];
|
|
13839
|
+
function contractHasPath(tree, segments) {
|
|
13840
|
+
if (segments.length === 0) return true;
|
|
13841
|
+
if (tree === true) return false;
|
|
13842
|
+
const [head, ...rest] = segments;
|
|
13843
|
+
if (head === void 0 || !Object.hasOwn(tree, head)) return false;
|
|
13844
|
+
const child = tree[head];
|
|
13845
|
+
return child === void 0 ? false : contractHasPath(child, rest);
|
|
13846
|
+
}
|
|
13847
|
+
function pruneContractPath(tree, segments) {
|
|
13848
|
+
if (tree === true || segments.length === 0) return tree;
|
|
13849
|
+
const [head, ...rest] = segments;
|
|
13850
|
+
if (head === void 0 || !Object.hasOwn(tree, head)) return tree;
|
|
13851
|
+
const next = { ...tree };
|
|
13852
|
+
if (rest.length === 0) {
|
|
13853
|
+
delete next[head];
|
|
13854
|
+
return next;
|
|
13855
|
+
}
|
|
13856
|
+
const child = next[head];
|
|
13857
|
+
if (child === void 0) return tree;
|
|
13858
|
+
next[head] = pruneContractPath(child, rest);
|
|
13859
|
+
return next;
|
|
13860
|
+
}
|
|
13861
|
+
function assertInboundDenyListResolves() {
|
|
13862
|
+
const unresolved = INBOUND_DENIED_PATHS.filter((entry) => {
|
|
13863
|
+
const tree = CLOUD_SYNC_CONTRACT[entry.namespace];
|
|
13864
|
+
return tree === void 0 || !contractHasPath(tree, entry.path.split("."));
|
|
13865
|
+
});
|
|
13866
|
+
if (unresolved.length > 0) {
|
|
13867
|
+
throw new Error(
|
|
13868
|
+
"INBOUND_DENIED_PATHS entr(ies) no longer resolve against CLOUD_SYNC_CONTRACT \u2014 a rename would silently re-open them: " + unresolved.map((entry) => `${entry.namespace}:${entry.path}`).join(", ")
|
|
13869
|
+
);
|
|
13870
|
+
}
|
|
13871
|
+
}
|
|
13872
|
+
var INBOUND_CONTRACT = (() => {
|
|
13873
|
+
assertInboundDenyListResolves();
|
|
13874
|
+
const out = { ...CLOUD_SYNC_CONTRACT };
|
|
13875
|
+
for (const entry of INBOUND_DENIED_PATHS) {
|
|
13876
|
+
const tree = out[entry.namespace];
|
|
13877
|
+
if (tree === void 0) continue;
|
|
13878
|
+
out[entry.namespace] = pruneContractPath(tree, entry.path.split("."));
|
|
13879
|
+
}
|
|
13880
|
+
return out;
|
|
13881
|
+
})();
|
|
13706
13882
|
var NAMESPACE_SCHEMA_VERSIONS = {
|
|
13707
13883
|
"core.runtime": 1,
|
|
13708
13884
|
"ui.preferences": 1,
|
|
@@ -13794,7 +13970,7 @@ function mergeAtContract(local, incoming, tree) {
|
|
|
13794
13970
|
return base;
|
|
13795
13971
|
}
|
|
13796
13972
|
function applyNamespacePayload(config, namespace, payload) {
|
|
13797
|
-
const tree =
|
|
13973
|
+
const tree = INBOUND_CONTRACT[namespace];
|
|
13798
13974
|
if (!tree || tree === true) return config;
|
|
13799
13975
|
const next = { ...config };
|
|
13800
13976
|
for (const [key, incoming] of Object.entries(payload)) {
|
package/dist/security/index.js
CHANGED
|
@@ -894,6 +894,24 @@ function validateDirectoryPolicy(value) {
|
|
|
894
894
|
import { homedir } from "node:os";
|
|
895
895
|
|
|
896
896
|
// src/security/secret-scrubber.ts
|
|
897
|
+
var JSON_CREDENTIAL_KEY_ANCHORS = [
|
|
898
|
+
'Key"',
|
|
899
|
+
'key"',
|
|
900
|
+
'KEY"',
|
|
901
|
+
'token"',
|
|
902
|
+
'Token"',
|
|
903
|
+
'TOKEN"',
|
|
904
|
+
'secret"',
|
|
905
|
+
'Secret"',
|
|
906
|
+
'SECRET"',
|
|
907
|
+
'password"',
|
|
908
|
+
'Password"',
|
|
909
|
+
'PASSWORD"',
|
|
910
|
+
'authorization"',
|
|
911
|
+
'Authorization"',
|
|
912
|
+
'bearer"',
|
|
913
|
+
'Bearer"'
|
|
914
|
+
];
|
|
897
915
|
var PATTERNS = [
|
|
898
916
|
// Anchored at the start where possible so partial matches inside larger
|
|
899
917
|
// strings don't trigger false positives.
|
|
@@ -996,6 +1014,30 @@ var PATTERNS = [
|
|
|
996
1014
|
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
997
1015
|
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
|
|
998
1016
|
},
|
|
1017
|
+
{
|
|
1018
|
+
type: "json_credential_key",
|
|
1019
|
+
// The JSON counterpart to `high_entropy_env`, and the pattern that the
|
|
1020
|
+
// now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
|
|
1021
|
+
// anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
|
|
1022
|
+
// "this text may hold a secret", every pattern then declined to match, and
|
|
1023
|
+
// the value went out verbatim. `high_entropy_env` cannot cover these: it
|
|
1024
|
+
// requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
|
|
1025
|
+
// never matched.
|
|
1026
|
+
//
|
|
1027
|
+
// Tool results are routinely serialised as JSON, and a credential with no
|
|
1028
|
+
// recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
|
|
1029
|
+
// has no other pattern that can catch it — this is the only thing standing
|
|
1030
|
+
// between such a value and the session JSONL, chronicle, HQ broadcast and
|
|
1031
|
+
// the model's own context.
|
|
1032
|
+
//
|
|
1033
|
+
// The key may carry a prefix (`"anthropicApiKey"`), but the credential word
|
|
1034
|
+
// must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
|
|
1035
|
+
// the closing quote has to follow the word immediately.
|
|
1036
|
+
// Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
|
|
1037
|
+
// out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
|
|
1038
|
+
regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
|
|
1039
|
+
anchor: JSON_CREDENTIAL_KEY_ANCHORS
|
|
1040
|
+
},
|
|
999
1041
|
// ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
|
|
1000
1042
|
// The plugin runtime carried 37 patterns while this scrubber — the one that
|
|
1001
1043
|
// guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
|
|
@@ -1078,9 +1120,12 @@ var PATTERNS = [
|
|
|
1078
1120
|
anchor: "GOCSPX-"
|
|
1079
1121
|
}
|
|
1080
1122
|
];
|
|
1081
|
-
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
1123
|
+
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
1124
|
+
(p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
|
|
1125
|
+
);
|
|
1082
1126
|
var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
|
|
1083
1127
|
var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
|
|
1128
|
+
var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
|
|
1084
1129
|
var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
|
|
1085
1130
|
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
1086
1131
|
var SCRUB_OVERLAP_BYTES = 1024;
|
|
@@ -1091,20 +1136,7 @@ var PATTERN_ANCHORS = [
|
|
|
1091
1136
|
)
|
|
1092
1137
|
)
|
|
1093
1138
|
];
|
|
1094
|
-
var
|
|
1095
|
-
'"apiKey"',
|
|
1096
|
-
'"api_key"',
|
|
1097
|
-
'"token"',
|
|
1098
|
-
'"secret"',
|
|
1099
|
-
'"password"',
|
|
1100
|
-
'"authorization"',
|
|
1101
|
-
'"bearer"',
|
|
1102
|
-
'"private_key"',
|
|
1103
|
-
'"access_token"',
|
|
1104
|
-
'"refresh_token"',
|
|
1105
|
-
'"client_secret"'
|
|
1106
|
-
];
|
|
1107
|
-
var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
|
|
1139
|
+
var ALL_ANCHORS = PATTERN_ANCHORS;
|
|
1108
1140
|
function hasCredentialAnchors(text) {
|
|
1109
1141
|
for (const anchor of ALL_ANCHORS) {
|
|
1110
1142
|
if (text.includes(anchor)) return true;
|
|
@@ -1153,6 +1185,9 @@ var DefaultSecretScrubber = class {
|
|
|
1153
1185
|
out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
|
|
1154
1186
|
return `${lead}${key}=[REDACTED:high_entropy_env]`;
|
|
1155
1187
|
});
|
|
1188
|
+
out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
|
|
1189
|
+
return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
|
|
1190
|
+
});
|
|
1156
1191
|
return out;
|
|
1157
1192
|
}
|
|
1158
1193
|
/**
|
|
@@ -1327,7 +1362,8 @@ async function evaluateToolKanbanBoundary(tool, input, ctx, options = {}) {
|
|
|
1327
1362
|
decision: "block",
|
|
1328
1363
|
reason: "Active card is not implementation-ready: " + readiness.issues.map((issue) => issue.message).join(" | "),
|
|
1329
1364
|
boardId: board.id,
|
|
1330
|
-
taskId: task.id
|
|
1365
|
+
taskId: task.id,
|
|
1366
|
+
readinessIssues: readiness.issues
|
|
1331
1367
|
};
|
|
1332
1368
|
}
|
|
1333
1369
|
if (task.lifecycle?.currentStage !== "running" || task.assignment?.status !== "running") {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { type KanbanBoundaryEvaluation } from '@wrongstack/kanban';
|
|
1
|
+
import { type KanbanBoundaryEvaluation, type KanbanContractReadinessIssue } from '@wrongstack/kanban';
|
|
2
2
|
import type { Context } from '../core/context.js';
|
|
3
3
|
import type { Tool } from '../types/tool.js';
|
|
4
4
|
export interface ToolKanbanBoundaryEvaluation extends KanbanBoundaryEvaluation {
|
|
5
5
|
boardId?: string | undefined;
|
|
6
6
|
taskId?: string | undefined;
|
|
7
|
+
/** Machine-readable readiness failures; `reason` remains the human fallback. */
|
|
8
|
+
readinessIssues?: KanbanContractReadinessIssue[] | undefined;
|
|
7
9
|
}
|
|
8
10
|
export interface ToolKanbanGovernanceOptions {
|
|
9
11
|
/** Require every product mutation to run inside a ready, running Kanban card. */
|
|
@@ -777,6 +777,24 @@ function loadDatabaseSync() {
|
|
|
777
777
|
}
|
|
778
778
|
|
|
779
779
|
// src/security/secret-scrubber.ts
|
|
780
|
+
var JSON_CREDENTIAL_KEY_ANCHORS = [
|
|
781
|
+
'Key"',
|
|
782
|
+
'key"',
|
|
783
|
+
'KEY"',
|
|
784
|
+
'token"',
|
|
785
|
+
'Token"',
|
|
786
|
+
'TOKEN"',
|
|
787
|
+
'secret"',
|
|
788
|
+
'Secret"',
|
|
789
|
+
'SECRET"',
|
|
790
|
+
'password"',
|
|
791
|
+
'Password"',
|
|
792
|
+
'PASSWORD"',
|
|
793
|
+
'authorization"',
|
|
794
|
+
'Authorization"',
|
|
795
|
+
'bearer"',
|
|
796
|
+
'Bearer"'
|
|
797
|
+
];
|
|
780
798
|
var PATTERNS = [
|
|
781
799
|
// Anchored at the start where possible so partial matches inside larger
|
|
782
800
|
// strings don't trigger false positives.
|
|
@@ -879,6 +897,30 @@ var PATTERNS = [
|
|
|
879
897
|
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
880
898
|
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
|
|
881
899
|
},
|
|
900
|
+
{
|
|
901
|
+
type: "json_credential_key",
|
|
902
|
+
// The JSON counterpart to `high_entropy_env`, and the pattern that the
|
|
903
|
+
// now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
|
|
904
|
+
// anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
|
|
905
|
+
// "this text may hold a secret", every pattern then declined to match, and
|
|
906
|
+
// the value went out verbatim. `high_entropy_env` cannot cover these: it
|
|
907
|
+
// requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
|
|
908
|
+
// never matched.
|
|
909
|
+
//
|
|
910
|
+
// Tool results are routinely serialised as JSON, and a credential with no
|
|
911
|
+
// recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
|
|
912
|
+
// has no other pattern that can catch it — this is the only thing standing
|
|
913
|
+
// between such a value and the session JSONL, chronicle, HQ broadcast and
|
|
914
|
+
// the model's own context.
|
|
915
|
+
//
|
|
916
|
+
// The key may carry a prefix (`"anthropicApiKey"`), but the credential word
|
|
917
|
+
// must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
|
|
918
|
+
// the closing quote has to follow the word immediately.
|
|
919
|
+
// Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
|
|
920
|
+
// out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
|
|
921
|
+
regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
|
|
922
|
+
anchor: JSON_CREDENTIAL_KEY_ANCHORS
|
|
923
|
+
},
|
|
882
924
|
// ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
|
|
883
925
|
// The plugin runtime carried 37 patterns while this scrubber — the one that
|
|
884
926
|
// guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
|
|
@@ -961,9 +1003,12 @@ var PATTERNS = [
|
|
|
961
1003
|
anchor: "GOCSPX-"
|
|
962
1004
|
}
|
|
963
1005
|
];
|
|
964
|
-
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
1006
|
+
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
1007
|
+
(p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
|
|
1008
|
+
);
|
|
965
1009
|
var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
|
|
966
1010
|
var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
|
|
1011
|
+
var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
|
|
967
1012
|
var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
|
|
968
1013
|
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
969
1014
|
var SCRUB_OVERLAP_BYTES = 1024;
|
|
@@ -974,20 +1019,7 @@ var PATTERN_ANCHORS = [
|
|
|
974
1019
|
)
|
|
975
1020
|
)
|
|
976
1021
|
];
|
|
977
|
-
var
|
|
978
|
-
'"apiKey"',
|
|
979
|
-
'"api_key"',
|
|
980
|
-
'"token"',
|
|
981
|
-
'"secret"',
|
|
982
|
-
'"password"',
|
|
983
|
-
'"authorization"',
|
|
984
|
-
'"bearer"',
|
|
985
|
-
'"private_key"',
|
|
986
|
-
'"access_token"',
|
|
987
|
-
'"refresh_token"',
|
|
988
|
-
'"client_secret"'
|
|
989
|
-
];
|
|
990
|
-
var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
|
|
1022
|
+
var ALL_ANCHORS = PATTERN_ANCHORS;
|
|
991
1023
|
function hasCredentialAnchors(text) {
|
|
992
1024
|
for (const anchor of ALL_ANCHORS) {
|
|
993
1025
|
if (text.includes(anchor)) return true;
|
|
@@ -1036,6 +1068,9 @@ var DefaultSecretScrubber = class {
|
|
|
1036
1068
|
out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
|
|
1037
1069
|
return `${lead}${key}=[REDACTED:high_entropy_env]`;
|
|
1038
1070
|
});
|
|
1071
|
+
out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
|
|
1072
|
+
return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
|
|
1073
|
+
});
|
|
1039
1074
|
return out;
|
|
1040
1075
|
}
|
|
1041
1076
|
/**
|
|
@@ -221,6 +221,24 @@ function loadDatabaseSync() {
|
|
|
221
221
|
}
|
|
222
222
|
|
|
223
223
|
// src/security/secret-scrubber.ts
|
|
224
|
+
var JSON_CREDENTIAL_KEY_ANCHORS = [
|
|
225
|
+
'Key"',
|
|
226
|
+
'key"',
|
|
227
|
+
'KEY"',
|
|
228
|
+
'token"',
|
|
229
|
+
'Token"',
|
|
230
|
+
'TOKEN"',
|
|
231
|
+
'secret"',
|
|
232
|
+
'Secret"',
|
|
233
|
+
'SECRET"',
|
|
234
|
+
'password"',
|
|
235
|
+
'Password"',
|
|
236
|
+
'PASSWORD"',
|
|
237
|
+
'authorization"',
|
|
238
|
+
'Authorization"',
|
|
239
|
+
'bearer"',
|
|
240
|
+
'Bearer"'
|
|
241
|
+
];
|
|
224
242
|
var PATTERNS = [
|
|
225
243
|
// Anchored at the start where possible so partial matches inside larger
|
|
226
244
|
// strings don't trigger false positives.
|
|
@@ -323,6 +341,30 @@ var PATTERNS = [
|
|
|
323
341
|
regex: /(^|\s)([A-Z_]{4,}(?:KEY|TOKEN|SECRET|PASSWORD|PWD))\s*[:=]\s*['"]?([A-Za-z0-9_/+=-]{20,512})['"]?(?=\s|$)/g,
|
|
324
342
|
anchor: ["KEY", "TOKEN", "SECRET", "PASSWORD", "PWD"]
|
|
325
343
|
},
|
|
344
|
+
{
|
|
345
|
+
type: "json_credential_key",
|
|
346
|
+
// The JSON counterpart to `high_entropy_env`, and the pattern that the
|
|
347
|
+
// now-deleted `JSON_KEY_ANCHORS` list was written for. Without it those
|
|
348
|
+
// anchors only widened the cheap pre-scan — `hasCredentialAnchors` said
|
|
349
|
+
// "this text may hold a secret", every pattern then declined to match, and
|
|
350
|
+
// the value went out verbatim. `high_entropy_env` cannot cover these: it
|
|
351
|
+
// requires an UPPERCASE unquoted key (`API_KEY=…`), so `{"apiKey":"…"}`
|
|
352
|
+
// never matched.
|
|
353
|
+
//
|
|
354
|
+
// Tool results are routinely serialised as JSON, and a credential with no
|
|
355
|
+
// recognisable prefix (Azure, self-hosted gateways, Anthropic/Codex OAuth)
|
|
356
|
+
// has no other pattern that can catch it — this is the only thing standing
|
|
357
|
+
// between such a value and the session JSONL, chronicle, HQ broadcast and
|
|
358
|
+
// the model's own context.
|
|
359
|
+
//
|
|
360
|
+
// The key may carry a prefix (`"anthropicApiKey"`), but the credential word
|
|
361
|
+
// must END the key: `"tokenCount"` and `"maxTokens"` do not match, because
|
|
362
|
+
// the closing quote has to follow the word immediately.
|
|
363
|
+
// Value floor of 8 chars keeps enum-ish values (`"authorization":"none"`)
|
|
364
|
+
// out. Capture groups: 1=key + punctuation, 2=value, 3=closing quote.
|
|
365
|
+
regex: /("[A-Za-z0-9_]*(?:apiKey|api_key|token|secret|password|authorization|bearer|private_key|access_token|refresh_token|client_secret)"\s*:\s*")([^"\\]{8,512})(")/gi,
|
|
366
|
+
anchor: JSON_CREDENTIAL_KEY_ANCHORS
|
|
367
|
+
},
|
|
326
368
|
// ── Ported from packages/plugins credential-patterns.ts (WS-034) ─────────
|
|
327
369
|
// The plugin runtime carried 37 patterns while this scrubber — the one that
|
|
328
370
|
// guards session JSONL, chronicle, HQ broadcast, WebUI events and the auth
|
|
@@ -405,9 +447,12 @@ var PATTERNS = [
|
|
|
405
447
|
anchor: "GOCSPX-"
|
|
406
448
|
}
|
|
407
449
|
];
|
|
408
|
-
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
450
|
+
var SIMPLE_PATTERNS = PATTERNS.filter(
|
|
451
|
+
(p) => p.type !== "high_entropy_env" && p.type !== "json_credential_key"
|
|
452
|
+
);
|
|
409
453
|
var COMBINED_REGEX = new RegExp(SIMPLE_PATTERNS.map((p) => `(${p.regex.source})`).join("|"), "g");
|
|
410
454
|
var HIGH_ENTROPY_REGEX = PATTERNS.find((p) => p.type === "high_entropy_env").regex;
|
|
455
|
+
var JSON_CREDENTIAL_REGEX = PATTERNS.find((p) => p.type === "json_credential_key").regex;
|
|
411
456
|
var COMBINED_REPLACEMENTS = SIMPLE_PATTERNS.map((p) => `[REDACTED:${p.type}]`);
|
|
412
457
|
var SCRUB_CHUNK_BYTES = 64 * 1024;
|
|
413
458
|
var SCRUB_OVERLAP_BYTES = 1024;
|
|
@@ -418,20 +463,7 @@ var PATTERN_ANCHORS = [
|
|
|
418
463
|
)
|
|
419
464
|
)
|
|
420
465
|
];
|
|
421
|
-
var
|
|
422
|
-
'"apiKey"',
|
|
423
|
-
'"api_key"',
|
|
424
|
-
'"token"',
|
|
425
|
-
'"secret"',
|
|
426
|
-
'"password"',
|
|
427
|
-
'"authorization"',
|
|
428
|
-
'"bearer"',
|
|
429
|
-
'"private_key"',
|
|
430
|
-
'"access_token"',
|
|
431
|
-
'"refresh_token"',
|
|
432
|
-
'"client_secret"'
|
|
433
|
-
];
|
|
434
|
-
var ALL_ANCHORS = [...PATTERN_ANCHORS, ...JSON_KEY_ANCHORS];
|
|
466
|
+
var ALL_ANCHORS = PATTERN_ANCHORS;
|
|
435
467
|
function hasCredentialAnchors(text) {
|
|
436
468
|
for (const anchor of ALL_ANCHORS) {
|
|
437
469
|
if (text.includes(anchor)) return true;
|
|
@@ -480,6 +512,9 @@ var DefaultSecretScrubber = class {
|
|
|
480
512
|
out = out.replace(HIGH_ENTROPY_REGEX, (_match, lead, key, _value) => {
|
|
481
513
|
return `${lead}${key}=[REDACTED:high_entropy_env]`;
|
|
482
514
|
});
|
|
515
|
+
out = out.replace(JSON_CREDENTIAL_REGEX, (_match, keyPrefix, _value, closingQuote) => {
|
|
516
|
+
return `${keyPrefix}[REDACTED:json_credential_key]${closingQuote}`;
|
|
517
|
+
});
|
|
483
518
|
return out;
|
|
484
519
|
}
|
|
485
520
|
/**
|