feat(FN-5035): merge fusion/fn-5035
This commit is contained in:
@@ -272,8 +272,7 @@ Public API surface:
|
||||
|
||||
Settings boundary:
|
||||
- Global default policy: `GlobalSettings.secretsAccessPolicy` (used by `resolveSecretAccessPolicy`).
|
||||
- Project-level secrets settings: `ProjectSettings.secretsEnv` and `ProjectSettings.secretsSyncPassphrase` (`packages/core/src/types.ts:2599-2609`).
|
||||
- `secretsSyncPassphrase` must be pre-wrapped ciphertext under local master key by caller (`packages/core/src/types.ts:2602-2604`).
|
||||
- Project-level secrets settings: `ProjectSettings.secretsEnv`. Cross-node sync passphrase state surfaces read-only via `GlobalSettings.secretsSyncPassphraseConfigured` (derived from `hasSyncPassphraseConfigured(secretsStore)` against the reserved `__sync_passphrase__` row in `secrets_global`).
|
||||
- Agent secret reads are exposed via `fn_secret_get` (`packages/cli/src/extension.ts:1542-1629`).
|
||||
- Cross-node sync routes ship at `/api/nodes/:id/secrets/push`, `/api/nodes/:id/secrets/pull`, `/api/secrets/sync-receive`, `/api/secrets/sync-export` with inbound Bearer apiKey validation (`packages/dashboard/src/routes/register-secrets-sync-inbound-routes.ts:99-114`, `:181-196`).
|
||||
|
||||
|
||||
@@ -34,16 +34,16 @@ Auth/ownership parity between settings-sync and secrets-sync routes is **impleme
|
||||
|
||||
## 4) Multi-node Behavior
|
||||
- Shared passphrase requirement is implementation-enforced via local reserved secret lookup (`getSyncPassphrase`) and unwrap failure semantics (`packages/core/src/secrets-sync-passphrase.ts:9-24`, `packages/core/src/secrets-sync.ts:107-110`). Docs state both nodes must share passphrase (`docs/secrets.md:115-121`).
|
||||
- Planning expected a `secretsSyncPassphrase` settings key, but shipped implementation uses reserved secret `__sync_passphrase__` + `getSyncPassphrase` instead (`packages/core/src/secrets-sync-passphrase.ts:1-24`). `docs/settings-reference.md` still labels `secretsSyncPassphrase` as planned (`docs/settings-reference.md:91,198`).
|
||||
- Planning expected a writable settings key, but shipped implementation stores the passphrase only in reserved secret `__sync_passphrase__` and now exposes state through read-only `GlobalSettings.secretsSyncPassphraseConfigured` (`packages/core/src/secrets-sync-passphrase.ts:1-24`).
|
||||
- No dedicated dashboard passphrase UX surfaced in routes/components reviewed; configuration remains implicit through secret storage primitives, so multi-node setup discoverability is partial.
|
||||
- Pull path handles remote envelope version mismatch because `unwrapSecretsBundle` throws `version-mismatch` on non-v1 (`packages/core/src/secrets-sync.ts:82-84`) and route maps `SecretsSyncError.code` to 400 (`register-secrets-sync-routes.ts:139-144`).
|
||||
- Audit payload hygiene: push logs only `{ nodeId, recordCount }` (`register-secrets-sync-routes.ts:92`); pull logs `{ nodeId, key, scope }` (`register-secrets-sync-routes.ts:180`); inbound receive logs `{ nodeId, key, scope }` (`register-secrets-sync-inbound-routes.ts:155-159`). No plaintext/ciphertext/passphrase fields are emitted.
|
||||
- `secretsSyncPassphrase` does not cross wire in route payload construction: outbound push sends envelope + `sourceNodeId`/`exportedAt` only (`register-secrets-sync-routes.ts:83-89`); inbound export returns envelope + metadata (`register-secrets-sync-inbound-routes.ts:209-213`). Passphrase itself is only used locally in wrap/unwrap calls.
|
||||
- Sync passphrase material does not cross wire in route payload construction: outbound push sends envelope + `sourceNodeId`/`exportedAt` only (`register-secrets-sync-routes.ts:83-89`); inbound export returns envelope + metadata (`register-secrets-sync-inbound-routes.ts:209-213`). Passphrase material is only used locally in wrap/unwrap calls.
|
||||
|
||||
## 5) Gaps & Follow-ups
|
||||
- [FN-4981] Add secrets-sync tests for outbound missing-remote-apiKey failure paths for push/pull. ✅ Landed (routes-secrets-sync now covers undefined/""/null apiKey plus guard-ordering invariant).
|
||||
- [FN-4980] Add secrets-sync tests for full inbound auth-cascade on `GET /api/secrets/sync-export`.
|
||||
- [FN-4982] Update stale settings-reference rows that still mark `secretsSyncPassphrase` as planned.
|
||||
- [FN-4982/FN-5035] Reconciled settings-reference docs with implementation: read-only global probe (`secretsSyncPassphraseConfigured`) + reserved-secret storage model.
|
||||
- [FN-4984] Add/clarify dashboard UX for configuring the shared sync passphrase (or equivalent guided setup).
|
||||
|
||||
## 6) References
|
||||
|
||||
@@ -20,7 +20,7 @@ Fusion's secrets subsystem provides encrypted-at-rest secret storage with projec
|
||||
| `secrets-sync.ts` wrap/unwrap core (scrypt → AES-256-GCM, version 1, typed errors) | Shipped | — | `packages/core/src/secrets-sync.ts` |
|
||||
| Dashboard `SecretsView` CRUD UI | Shipped | — | `packages/dashboard/app/components/SecretsView.tsx` |
|
||||
| `secretsEnv.*` settings + worktree `.env` materialization + fingerprint cleanup | Shipped | — | `packages/core/src/types.ts`, `packages/engine/src/secrets-env-writer.ts` |
|
||||
| `secretsSyncPassphrase` project setting integration | Shipped | — | `packages/core/src/types.ts`, `packages/dashboard/src/routes/register-secrets-sync-routes.ts` |
|
||||
| `secretsSyncPassphraseConfigured` global read-only probe + reserved secret storage (`__sync_passphrase__`) | Shipped | — | `packages/core/src/types.ts`, `packages/core/src/secrets-sync-passphrase.ts` |
|
||||
| Cross-node sync REST endpoints (`/api/nodes/:id/secrets/{push,pull}`, `/api/secrets/sync-receive`, `/api/secrets/sync-export`) | Shipped | — | `packages/dashboard/src/routes/register-secrets-sync-routes.ts`, `packages/dashboard/src/routes/register-secrets-sync-inbound-routes.ts` |
|
||||
| Audit-event registration on `FilesystemMutationType` for `secret:env-*` and `secret:sync-*` | Shipped | — | `packages/engine/src/run-audit.ts` |
|
||||
| Master-key rotation UX | Pending | — | n/a |
|
||||
@@ -133,14 +133,15 @@ Fusion can materialize env-exportable secrets into each acquired task worktree w
|
||||
- Fingerprint sidecar: successful writes persist `.fusion-secrets-env.fingerprint` containing `<sha256>\n<filename>\n` (mode `0o600`) so teardown can verify file integrity before deletion.
|
||||
- Teardown cleanup: when a worktree is removed, Fusion deletes the managed env file only when the on-disk fingerprint still matches; edited files are preserved and only the sidecar is removed.
|
||||
|
||||
Settings shape is project-scoped in `ProjectSettings` (`packages/core/src/types.ts:2599-2609`): `secretsEnv` (env materialization config) and `secretsSyncPassphrase` (ciphertext already wrapped under local master key by caller; see `types.ts:2602-2604`).
|
||||
Settings shape is split by scope: project-level secrets settings are limited to `ProjectSettings.secretsEnv`, while cross-node sync passphrase state is stored only as the reserved `__sync_passphrase__` row in `secrets_global` and exposed read-only through `GlobalSettings.secretsSyncPassphraseConfigured` (`packages/core/src/types.ts`). Settings never carry the plaintext passphrase.
|
||||
|
||||
### Test locations
|
||||
|
||||
The settings contract (`SecretsEnvSettings` shape, defaults, project round-trip) is covered in `@fusion/core`:
|
||||
|
||||
- `packages/core/src/__tests__/secrets-env.test.ts` — type contract + defaults
|
||||
- `packages/core/src/__tests__/store-settings.test.ts` — `secretsEnv` + `secretsSyncPassphrase` project round-trip
|
||||
- `packages/core/src/__tests__/store-settings.test.ts` — `secretsEnv` project round-trip
|
||||
- `packages/core/src/__tests__/store-settings-sync-passphrase-probe.test.ts` — read-only `secretsSyncPassphraseConfigured` derivation + write-strip behavior
|
||||
|
||||
The materialization implementation lives in `@fusion/engine` and is covered there:
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ Fusion automatically falls back to ntfy's JSON publish format when a notificatio
|
||||
| `settingsSyncInterval` | `number` | `900000` | Automatic sync interval in ms. Valid values: `300000`, `900000`, `1800000`, `3600000`. |
|
||||
| `settingsSyncConflictResolution` | `"last-write-wins" \| "always-ask" \| "keep-local" \| "keep-remote"` | `"last-write-wins"` | Conflict strategy for divergent synced settings. |
|
||||
| `secretsAccessPolicy` | `"auto" \| "prompt" \| "deny"` | `undefined` | Global default secret access policy used when a secret row does not set `access_policy`; resolver fallback remains `"prompt"`. |
|
||||
| `secretsSyncPassphraseConfigured` | `boolean` | `false` | Read-only global probe for cross-node secrets-sync passphrase presence. Derived from `hasSyncPassphraseConfigured(secretsStore)` against the reserved `__sync_passphrase__` row in `secrets_global`. Not writable through settings APIs and never includes plaintext. |
|
||||
| `owningNodeHandoffPolicy` | `"block" \| "reassign-to-local" \| "reassign-any-healthy"` | `"reassign-to-local"` | Global fallback policy for tasks whose owning checkout node is unavailable. Project-level `owningNodeHandoffPolicy` overrides this. |
|
||||
| `dashboardCurrentNodeId` | `string` | `undefined` | Currently selected dashboard node ID. Restores the last-viewed node on fresh browser/PWA sessions. `undefined` means viewing the local node. |
|
||||
|
||||
@@ -193,7 +194,6 @@ Defaults from `DEFAULT_PROJECT_SETTINGS`; key scope from `PROJECT_SETTINGS_KEYS`
|
||||
| `unavailableNodePolicy` | `"block" \| "fallback-local"` | `"block"` | Project routing policy used during scheduler dispatch when a task resolves to a remote node and node health is known. `"block"` keeps the task in `todo` if the node is unhealthy; `"fallback-local"` reroutes dispatch to local execution. See [Architecture → Task Routing Architecture](./architecture.md#task-routing-architecture). |
|
||||
| `secretsAccessPolicy` | `"auto" \| "prompt" \| "deny"` | `undefined` | Project-level default secret access policy (overrides global default when present). |
|
||||
| `secretsEnv` | `{ enabled?: boolean; filename?: string; overwritePolicy?: "skip" \| "merge" \| "replace"; keyPrefix?: string; requireGitignored?: boolean }` | `undefined` | Per-project secrets `.env` materialization configuration. When `enabled`, the engine writes `secretsEnv.filename` (default `.env`) into each acquired task worktree from secrets marked `env_exportable=true`. `overwritePolicy` controls merge/skip/replace against an existing file; `requireGitignored` (default `true`) refuses to write a non-gitignored path; `keyPrefix` filters which exported keys are included. See [Secrets](./secrets.md#env-auto-write-into-worktrees). |
|
||||
| `secretsSyncPassphrase` | `string` | `undefined` | Per-project shared passphrase for cross-node secrets sync envelope. Stored locally encrypted under the master key (reserved `__sync_passphrase__` row in `secrets_global`, `access_policy="deny"`) and never transmitted; only the derived envelope crosses the wire. See [Secrets](./secrets.md#cross-node-sync). |
|
||||
| `owningNodeHandoffPolicy` | `"block" \| "reassign-to-local" \| "reassign-any-healthy"` | `"reassign-to-local"` | Policy for tasks already checked out by an unavailable owning node. `"block"` parks, `"reassign-to-local"` takes over on local node, `"reassign-any-healthy"` makes takeover eligible on healthy peers. |
|
||||
|
||||
| `groupOverlappingFiles` | `boolean` | `true` | Serialize execution when file scopes overlap. |
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createTestProject } from "./test-project.js";
|
||||
import { clearSyncPassphrase, setSyncPassphrase } from "../secrets-sync-passphrase.js";
|
||||
import { CentralCore } from "../central-core.js";
|
||||
import { MasterKeyManager } from "../master-key.js";
|
||||
import { SecretsStore } from "../secrets-store.js";
|
||||
|
||||
async function createSecretsStore(fixture: Awaited<ReturnType<typeof createTestProject>>): Promise<SecretsStore> {
|
||||
const central = new CentralCore(fixture.globalDir);
|
||||
await central.init();
|
||||
const centralDb = (central as unknown as { db: import("../central-db.js").CentralDatabase | null }).db;
|
||||
if (!centralDb) throw new Error("central db unavailable");
|
||||
const masterKeyManager = new MasterKeyManager({ globalDir: fixture.globalDir });
|
||||
return new SecretsStore(fixture.store.getDatabase(), centralDb, () => masterKeyManager.getOrCreateKey());
|
||||
}
|
||||
|
||||
describe("TaskStore secretsSyncPassphraseConfigured probe", () => {
|
||||
it("returns false when reserved passphrase row is absent", async () => {
|
||||
const fixture = await createTestProject();
|
||||
try {
|
||||
expect((await fixture.store.getSettings()).secretsSyncPassphraseConfigured).toBe(false);
|
||||
expect((await fixture.store.getSettingsFast()).secretsSyncPassphraseConfigured).toBe(false);
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("flips false -> true -> false as passphrase is set and cleared", async () => {
|
||||
const fixture = await createTestProject();
|
||||
try {
|
||||
const secrets = await createSecretsStore(fixture);
|
||||
const spy = vi.spyOn(fixture.store, "getSecretsStore").mockResolvedValue(secrets);
|
||||
expect((await fixture.store.getSettings()).secretsSyncPassphraseConfigured).toBe(false);
|
||||
|
||||
await setSyncPassphrase(secrets, "pp");
|
||||
expect((await fixture.store.getSettings()).secretsSyncPassphraseConfigured).toBe(true);
|
||||
expect((await fixture.store.getSettingsFast()).secretsSyncPassphraseConfigured).toBe(true);
|
||||
|
||||
await clearSyncPassphrase(secrets);
|
||||
expect((await fixture.store.getSettings()).secretsSyncPassphraseConfigured).toBe(false);
|
||||
expect((await fixture.store.getSettingsFast()).secretsSyncPassphraseConfigured).toBe(false);
|
||||
spy.mockRestore();
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes probe under global scope only", async () => {
|
||||
const fixture = await createTestProject();
|
||||
try {
|
||||
const byScope = await fixture.store.getSettingsByScope();
|
||||
const byScopeFast = await fixture.store.getSettingsByScopeFast();
|
||||
expect(byScope.global.secretsSyncPassphraseConfigured).toBe(false);
|
||||
expect(byScopeFast.global.secretsSyncPassphraseConfigured).toBe(false);
|
||||
expect(byScope.project).not.toHaveProperty("secretsSyncPassphraseConfigured");
|
||||
expect(byScopeFast.project).not.toHaveProperty("secretsSyncPassphraseConfigured");
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not persist writable overrides from updateSettings", async () => {
|
||||
const fixture = await createTestProject();
|
||||
try {
|
||||
await fixture.store.updateSettings({ secretsSyncPassphraseConfigured: true });
|
||||
expect((await fixture.store.getSettings()).secretsSyncPassphraseConfigured).toBe(false);
|
||||
|
||||
await fixture.store.updateGlobalSettings({ secretsSyncPassphraseConfigured: true });
|
||||
expect((await fixture.store.getSettings()).secretsSyncPassphraseConfigured).toBe(false);
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("falls back to false when secrets store lookup throws", async () => {
|
||||
const fixture = await createTestProject();
|
||||
try {
|
||||
const spy = vi.spyOn(fixture.store, "getSecretsStore").mockRejectedValueOnce(new Error("boom"));
|
||||
await expect(fixture.store.getSettings()).resolves.toMatchObject({ secretsSyncPassphraseConfigured: false });
|
||||
spy.mockRestore();
|
||||
} finally {
|
||||
await fixture.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -53,7 +53,7 @@ describe("TaskStore", () => {
|
||||
});
|
||||
|
||||
describe("secrets integration settings", () => {
|
||||
it("round-trips secretsEnv and secretsSyncPassphrase via project settings", async () => {
|
||||
it("round-trips secretsEnv via project settings", async () => {
|
||||
await harness.store().updateSettings({
|
||||
secretsEnv: {
|
||||
enabled: true,
|
||||
@@ -62,7 +62,6 @@ describe("TaskStore", () => {
|
||||
keyPrefix: "FUSION_",
|
||||
requireGitignored: true,
|
||||
},
|
||||
secretsSyncPassphrase: "encrypted:abc123",
|
||||
});
|
||||
|
||||
const settings = await harness.store().getSettings();
|
||||
@@ -73,11 +72,9 @@ describe("TaskStore", () => {
|
||||
keyPrefix: "FUSION_",
|
||||
requireGitignored: true,
|
||||
});
|
||||
expect(settings.secretsSyncPassphrase).toBe("encrypted:abc123");
|
||||
|
||||
const { project } = await harness.store().getSettingsByScope();
|
||||
expect(project.secretsEnv?.enabled).toBe(true);
|
||||
expect(project.secretsSyncPassphrase).toBe("encrypted:abc123");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -100,6 +100,7 @@ export const DEFAULT_GLOBAL_SETTINGS = {
|
||||
persistAgentThinkingLog: false,
|
||||
agentMemoryInclusionMode: "full",
|
||||
secretsAccessPolicy: undefined,
|
||||
secretsSyncPassphraseConfigured: false,
|
||||
researchGlobalDefaults: {
|
||||
searchProvider: undefined,
|
||||
synthesisProvider: undefined,
|
||||
@@ -200,7 +201,6 @@ export const DEFAULT_PROJECT_SETTINGS = {
|
||||
owningNodeHandoffPolicy: "reassign-to-local",
|
||||
defaultNodeId: undefined,
|
||||
secretsEnv: undefined,
|
||||
secretsSyncPassphrase: undefined,
|
||||
worktreeInitCommand: undefined,
|
||||
testCommand: undefined,
|
||||
buildCommand: undefined,
|
||||
|
||||
@@ -25,6 +25,7 @@ import { BackwardCompat, ProjectRequiredError } from "./migration.js";
|
||||
import { CentralCore } from "./central-core.js";
|
||||
import { SecretsStore } from "./secrets-store.js";
|
||||
import { MasterKeyManager } from "./master-key.js";
|
||||
import { hasSyncPassphraseConfigured } from "./secrets-sync-passphrase.js";
|
||||
import { getTaskMergeBlocker, resolveTaskMergeTarget } from "./task-merge.js";
|
||||
import { getInReviewStallReason } from "./in-review-stall.js";
|
||||
import { getStalePausedReviewSignal } from "./stale-paused-review.js";
|
||||
@@ -2343,6 +2344,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
(projectSettings as Partial<Settings>).worktrunk,
|
||||
),
|
||||
};
|
||||
try {
|
||||
merged.secretsSyncPassphraseConfigured = await hasSyncPassphraseConfigured(await this.getSecretsStore());
|
||||
} catch {
|
||||
merged.secretsSyncPassphraseConfigured = false;
|
||||
}
|
||||
return canonicalizeSettings(merged);
|
||||
}
|
||||
|
||||
@@ -2382,6 +2388,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
...projectSettings,
|
||||
worktrunk: resolveWorktrunkSettings(globalSettings.worktrunk, projectSettings?.worktrunk),
|
||||
};
|
||||
try {
|
||||
merged.secretsSyncPassphraseConfigured = await hasSyncPassphraseConfigured(await this.getSecretsStore());
|
||||
} catch {
|
||||
merged.secretsSyncPassphraseConfigured = false;
|
||||
}
|
||||
|
||||
return canonicalizeSettings(merged);
|
||||
}
|
||||
@@ -2398,6 +2409,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
this.globalSettingsStore.getSettings(),
|
||||
this.readConfig(),
|
||||
]);
|
||||
try {
|
||||
globalSettings.secretsSyncPassphraseConfigured = await hasSyncPassphraseConfigured(await this.getSecretsStore());
|
||||
} catch {
|
||||
globalSettings.secretsSyncPassphraseConfigured = false;
|
||||
}
|
||||
|
||||
// Extract only project-level keys from config.settings
|
||||
const projectSettings: Partial<ProjectSettings> = {};
|
||||
@@ -2435,6 +2451,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
this.globalSettingsStore.getSettings(),
|
||||
this.db.prepare("SELECT settings FROM config WHERE id = 1").get() as { settings?: string } | undefined,
|
||||
]);
|
||||
try {
|
||||
globalSettings.secretsSyncPassphraseConfigured = await hasSyncPassphraseConfigured(await this.getSecretsStore());
|
||||
} catch {
|
||||
globalSettings.secretsSyncPassphraseConfigured = false;
|
||||
}
|
||||
|
||||
const projectSettings = row?.settings ? fromJson<Settings>(row.settings) : undefined;
|
||||
|
||||
@@ -2567,6 +2588,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
const previous: Settings = { ...DEFAULT_SETTINGS, ...previousGlobal, ...config.settings } as Settings;
|
||||
|
||||
const globalPatch: Partial<GlobalSettings> = { ...patch };
|
||||
delete globalPatch.secretsSyncPassphraseConfigured;
|
||||
|
||||
// Handle deep merge + targeted null clear semantics for remoteAccess
|
||||
const incomingRemoteAccess = (globalPatch as Record<string, unknown>)["remoteAccess"];
|
||||
@@ -2609,6 +2631,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
const updatedGlobal = await this.globalSettingsStore.updateSettings(globalPatch);
|
||||
const merged: Settings = { ...DEFAULT_SETTINGS, ...updatedGlobal, ...config.settings } as Settings;
|
||||
try {
|
||||
merged.secretsSyncPassphraseConfigured = await hasSyncPassphraseConfigured(await this.getSecretsStore());
|
||||
} catch {
|
||||
merged.secretsSyncPassphraseConfigured = false;
|
||||
}
|
||||
|
||||
// Emit settings:updated so SSE listeners pick up the change
|
||||
this.emit("settings:updated", { settings: merged, previous });
|
||||
|
||||
@@ -2150,6 +2150,11 @@ export interface GlobalSettings {
|
||||
* "prompt" (route through approvals), or "deny" (reject without prompt).
|
||||
* Default when unset: "prompt". */
|
||||
secretsAccessPolicy?: SecretAccessPolicy;
|
||||
/** Read-only derived probe for cross-node secrets sync passphrase state.
|
||||
* Mirrors `hasSyncPassphraseConfigured(secretsStore)` against the reserved
|
||||
* `__sync_passphrase__` row in `secrets_global`. Never includes plaintext and
|
||||
* cannot be persisted via `updateSettings` / `updateGlobalSettings`. */
|
||||
secretsSyncPassphraseConfigured?: boolean;
|
||||
/** Policy for recovering tasks whose existing owning node becomes unavailable. */
|
||||
owningNodeHandoffPolicy?: OwningNodeHandoffPolicy;
|
||||
/** How long a task must remain in `status='failed'` before a push notification fires.
|
||||
@@ -2624,16 +2629,6 @@ export interface ProjectSettings {
|
||||
researchSettings?: ResearchProjectSettings;
|
||||
/** Optional per-project `.env` materialization settings for exportable secrets. */
|
||||
secretsEnv?: SecretsEnvSettings;
|
||||
/**
|
||||
* Encrypted shared-passphrase blob used for cross-node secrets sync.
|
||||
* The stored value MUST already be ciphertext wrapped under the local
|
||||
* master key by the caller/settings writer; the settings store does not
|
||||
* automatically wrap this field.
|
||||
*
|
||||
* This value MUST NEVER be transmitted over the network; only derived
|
||||
* sync envelopes may cross the wire.
|
||||
*/
|
||||
secretsSyncPassphrase?: string;
|
||||
/** Sandbox command-execution settings.
|
||||
* When omitted, runtime behavior is preserved via native passthrough defaults. */
|
||||
sandbox?: SandboxProjectSettings;
|
||||
|
||||
Reference in New Issue
Block a user