Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore: Unused imports
import * as seedorigin$0 from "../seedorigin/models.js";

/**
* Effect is a rule's (or the final evaluation's) verdict. Ordered by
* precedence: deny always beats ask, ask always beats allow, no matter
Expand Down Expand Up @@ -97,4 +101,19 @@ export interface Rule {
* references a rule by ID).
*/
"Source": string;

/**
* BuiltIn marks a seeded example rule (BuiltIn() below) -- purely
* informational, same as mcpserver.MCPServer.BuiltIn/execenv.
* ExecEnv.BuiltIn: drives a "built-in" badge only, never gates
* Edit/Delete.
*/
"BuiltIn": boolean;

/**
* Seed is this rule's seed provenance (docs/goals/0037) -- zero
* value means "not of seed origin," migration-free. See
* composition.Workflow.Seed's doc comment for the full reasoning.
*/
"Seed": seedorigin$0.Origin;
}
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,20 @@ export function DeleteMCPServer(id: string): $CancellablePromise<void> {
return $Call.ByID(3847603582, id);
}

/**
* DeriveSecretLabels answers, statically from a node's own type and
* config -- never by resolving a real secret VALUE -- which vault
* entries its execution will resolve, as sorted, deduped LABELS (goal
* 0203 S2). This must compute the exact same answer a real run's own
* vault-reference resolution (vaultref.go) would reach: WorkflowVerdicts
* (the canvas nothing-hidden badge) calls this before anyone runs the
* workflow, so a step that will actually touch a secret can never show
* a clean badge the live gate then contradicts.
*/
export function DeriveSecretLabels(nodeTypeID: string, config: { [_ in string]?: string } | null): $CancellablePromise<string[] | null> {
return $Call.ByID(3138098164, nodeTypeID, config);
}

export function ExecEnvs(): $CancellablePromise<execenv$0.ExecEnv[] | null> {
return $Call.ByID(1392956673);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ export function CreateRule(rule: guardrail$0.Rule): $CancellablePromise<guardrai
* DeleteRule removes a rule by ID; deleting an absent rule is a no-op,
* matching every other Configure entity's delete semantics. Returns the
* persist error (rather than swallowing it, docs/goals/0025 item 1) and
* restores the deleted rule if the store write fails.
* restores the deleted rule if the store write (or, for a built-in
* rule, the tombstone write, goal 0203 S2) fails.
*/
export function DeleteRule(id: string): $CancellablePromise<void> {
return $Call.ByID(1475597571, id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@ export {
};

export type {
ListSecretAccessRequest,
ListSecretAccessResponse,
SecretAccessRecord,
Status
} from "./models.js";
Original file line number Diff line number Diff line change
@@ -1,6 +1,46 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT

/**
* ListSecretAccessRequest is the bound read API's request shape --
* EntryID empty means "no filter" (the Secrets view's global Access
* history list); set means "this one entry's own history" (the detail
* dialog's own filtered view). Mirrors mcpauditsvc.ListMCPCallsRequest's
* own shape.
*/
export interface ListSecretAccessRequest {
"entryId": string;
"limit": number;
"offset": number;
}

/**
* ListSecretAccessResponse carries one page plus the total matching-row
* count, same "showing X-Y of Z" reasoning ListMCPCallsResponse gives.
*/
export interface ListSecretAccessResponse {
"records": SecretAccessRecord[] | null;
"total": number;
}

/**
* SecretAccessRecord is the frontend-facing JSON shape for one audit
* row -- mirrors secretaudit.Record with JSON tags added, same
* "adapter type stays free of a frontend-JSON concern" reasoning
* mcpauditsvc.MCPCallRecord's own doc comment gives.
*/
export interface SecretAccessRecord {
"id": number;
"timestamp": string;
"entryId": string;
"label": string;
"context": string;
"runId": string;
"workflowId": string;
"outcome": string;
"errorText": string;
}

/**
* Status is VaultStatus's return shape -- the one read the frontend
* needs to decide which of "set up," "unlock," or "browse" to show.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import * as $models from "./models.js";
* but ONLY if the clipboard still holds exactly that value at that
* point (the same "don't clobber something the user copied since"
* check KeePassXC's own auto-clear makes), never unconditionally.
* Records one ContextUICopy audit line (goal 0203 S3), same not-gated-
* but-visible posture as RevealSecret.
*/
export function CopySecretToClipboard(id: string): $CancellablePromise<void> {
return $Call.ByID(2769882406, id);
Expand Down Expand Up @@ -55,6 +57,19 @@ export function GeneratePassword(length: number, upper: boolean, lower: boolean,
return $Call.ByID(2111800646, length, upper, lower, digits, symbols);
}

/**
* ListSecretAccess is the bound read API the Secrets view's Access
* history list calls -- newest first, optionally filtered to one entry,
* limit/offset paged. Returns an empty page (never an error) when no
* audit store is wired yet -- structurally unreachable in the real app
* (main.go wires OpenAudit before any window/frontend exists), kept
* graceful rather than surfacing a confusing error on a race that can't
* actually happen.
*/
export function ListSecretAccess(req: $models.ListSecretAccessRequest): $CancellablePromise<$models.ListSecretAccessResponse> {
return $Call.ByID(2793339806, req);
}

/**
* ListSecrets returns every entry's masked Summary (no password) --
* the browse surface's own list, sorted by title
Expand All @@ -81,6 +96,12 @@ export function LockVault(): $CancellablePromise<void> {
* good way to surface a SECOND error about redaction failing while
* already reporting a first one -- text passes through unredacted
* rather than the whole error path failing outright.
*
* Deliberately unaudited (goal 0203 S3 contract): this reads every
* vault entry on a failure path purely to SCRUB output, never to expose
* a value to anyone -- recording it would bury real reads (a workflow
* that actually used a credential) under one audit line per error
* message formatted anywhere in the app.
*/
export function RedactKnownSecrets(text: string): $CancellablePromise<string> {
return $Call.ByID(2598084233, text);
Expand All @@ -92,7 +113,9 @@ export function RedactKnownSecrets(text: string): $CancellablePromise<string> {
* browsing), matching SetHTTPRequestSecret's own write-only-elsewhere
* posture but inverted: this vault's whole point is a human can read
* their own password back, unlike the write-only integration-secret
* slots.
* slots. Records one ContextUIReveal audit line (goal 0203 S3) -- a
* human's own click, not gated (S2 contract), but visible in their own
* Access history.
*/
export function RevealSecret(id: string): $CancellablePromise<secret$0.Entry> {
return $Call.ByID(1414222929, id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,12 @@ export function AutoUpdateCheck(): $CancellablePromise<boolean> {

/**
* CheckForUpdates asks the configured provider (GitHub Releases,
* alicoding/mill) whether a newer version exists.
* alicoding/mill) whether a newer version exists. The Wails-bound RPC
* surface (no context parameter is bindable); checkForUpdates below
* does the real work against a caller-supplied context, letting the
* background loop (settingsservice_updatenotice.go) propagate its own
* cancellable context instead of a fresh context.Background() call
* happening underneath it.
*/
export function CheckForUpdates(): $CancellablePromise<$models.UpdateCheckResult> {
return $Call.ByID(3825907183);
Expand Down Expand Up @@ -494,7 +499,11 @@ export function SetAttentionIdleThreshold(seconds: number): $CancellablePromise<
}

/**
* SetAutoUpdateCheck persists the opt-in; applies at boot.
* SetAutoUpdateCheck persists the opt-in and applies it live (goal
* 0207): turning it on starts the background loop immediately if it
* isn't already running; turning it off stops it. Both directions are
* idempotent -- flipping the same value twice is a no-op the second
* time, never a second loop or a panic on double-stop.
*/
export function SetAutoUpdateCheck(on: boolean): $CancellablePromise<void> {
return $Call.ByID(3808971894, on);
Expand Down
35 changes: 34 additions & 1 deletion frontend/e2e/secrets.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,21 @@ test('secret manager: create vault, store/reveal/copy/edit/history/delete a pass
await expect(page.getByTestId('secret-detail-copied').or(page.getByTestId('secret-detail-error'))).toBeVisible()
})

// --- Access history (goal 0203 S3): the reveal (and, when the real
// clipboard write succeeded, the copy) just performed each leave a
// visible row, in the user's own vocabulary, from the entry's own
// filtered view. Scoped to the currently-open detail dialog: the
// Secrets view header's own global Access history button carries
// the identical accessible name, still present (unmounted) behind
// this modal. ---
const bankDetailDialog = page.getByRole('dialog', { name: 'Bank of Testing', exact: true })
await bankDetailDialog.getByRole('button', { name: 'Access history' }).click()
const entryAccessHistory = page.getByRole('dialog', { name: /Access history for/ })
await expect(entryAccessHistory).toBeVisible()
await expect(entryAccessHistory.getByText('Shown to you')).toBeVisible()
await entryAccessHistory.getByLabel('Close').click()
await expect(bankDetailDialog).toBeVisible()

// --- Edit: change the password ---
await page.getByRole('button', { name: 'Edit' }).click()
const editPassword = page.getByTestId('secret-password-input')
Expand All @@ -112,14 +127,32 @@ test('secret manager: create vault, store/reveal/copy/edit/history/delete a pass
await expect(page.getByTestId('secret-detail-password')).toHaveValue('second-password-fake')

// --- History: the pre-edit value is preserved ---
await page.getByRole('button', { name: 'History' }).click()
// exact: true -- "History" is otherwise a substring match against
// this same dialog's own "Access history" footer button.
await page.getByRole('button', { name: 'History', exact: true }).click()
const historyRow = page.getByTestId('secret-history-row')
await expect(historyRow).toHaveCount(1)
await historyRow.getByLabel('Show password').click()
await expect(historyRow.locator('input')).toHaveValue('first-password-fake')
await page.getByRole('dialog', { name: /History for/ }).getByLabel('Close').click()
await page.getByRole('dialog', { name: 'Bank of Testing', exact: true }).getByLabel('Close').click()

// --- Global Access history (Secrets view header, goal 0203 S3):
// every read/reveal/copy this run performed against "Bank of
// Testing" shows up, newest first, each carrying the entry's own
// label -- unlike the per-entry filtered view above, which shows
// context only, this list needs the label to tell entries apart. ---
await page.getByTestId('secrets-access-history-open').click()
const globalAccessHistory = page.getByRole('dialog', { name: 'Access history', exact: true })
await expect(globalAccessHistory).toBeVisible()
await expect(globalAccessHistory.getByText('Bank of Testing').first()).toBeVisible()
// Copied to the clipboard vs. Couldn't be read -- environment-
// dependent, same reasoning the earlier real-clipboard step's own
// comment gives (headless Linux CI has no pbcopy); either one
// proves the copy attempt left a row.
await expect(globalAccessHistory.getByText('Copied to the clipboard').or(globalAccessHistory.getByText("Couldn't be read"))).toBeVisible()
await globalAccessHistory.getByLabel('Close').click()

// --- Delete via the row's kebab menu, confirmed by name ---
const bankRow = page.getByTestId('inventory-row').filter({ hasText: 'Bank of Testing' })
await bankRow.getByTestId('inventory-row-menu').click()
Expand Down
1 change: 1 addition & 0 deletions frontend/src/composition/breakpoints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ export function useBreakpoints(workflowId: string | undefined, onChanged?: () =>
GuardrailService.CreateRule({
ID: '', Label: 'Breakpoint', Effect: GuardrailEffect.EffectAsk, Source: DEBUG_SOURCE,
WorkflowID: workflowId, NodeID: nodeId, NodeTypeID: '', RequestID: '', Condition: '',
BuiltIn: false, Seed: { SeedRevision: 0, Modified: false },
}).then(done).catch(done)
}
},
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/locales/en/secrets.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,26 @@
"urlLabel": "Website",
"notesLabel": "Notes",
"tagsLabel": "Tags"
},
"accessHistory": {
"button": "Access history",
"heading": "Access history",
"headingForEntry": "Access history for \"{{label}}\"",
"emptyHeading": "No reads yet",
"emptyDescription": "Every time this vault is read, it shows up here.",
"showingRange": "Showing {{start}}-{{end}} of {{total}}",
"previousPageAriaLabel": "Previous page",
"nextPageAriaLabel": "Next page",
"outcomeRead": "Read",
"outcomeError": "Couldn't be read",
"readFailed": "Couldn't be read",
"readByWorkflow": "Read by workflow \"{{label}}\"",
"readMcpServerSpawn": "Read to start an MCP server",
"readExecEnv": "Read to run a command",
"readHttpHeader": "Read for an API call",
"readConfigureToolsPreview": "Read to list an MCP server's tools",
"readUiReveal": "Shown to you",
"readUiCopy": "Copied to the clipboard",
"readGeneric": "Read"
}
}
1 change: 1 addition & 0 deletions frontend/src/shared/GuardrailRuleDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export function GuardrailRuleDialog({ rule, onClose, onSaved }: {
await GuardrailService.CreateRule({
ID: '', Label: label, Effect: effect as GuardrailEffect, Condition: condition, Source: '',
NodeTypeID: '', RequestID: '', WorkflowID: '', NodeID: '',
BuiltIn: false, Seed: { SeedRevision: 0, Modified: false },
...scopeFields,
})
}
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/shared/bindings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ export type {
PairingCodeInfo,
} from '../../bindings/github.com/alicoding/mill/internal/services/remoteauthsvc'
export { SecretService } from '../../bindings/github.com/alicoding/mill/internal/services/secretsvc'
export type { Status as VaultStatus } from '../../bindings/github.com/alicoding/mill/internal/services/secretsvc/models'
export type {
ListSecretAccessRequest,
ListSecretAccessResponse,
SecretAccessRecord,
Status as VaultStatus,
} from '../../bindings/github.com/alicoding/mill/internal/services/secretsvc/models'
export type { Entry as SecretEntry, Summary as SecretSummary } from '../../bindings/github.com/alicoding/mill/internal/domain/secret/models'
export { SettingsService } from '../../bindings/github.com/alicoding/mill/internal/services/settingssvc'
export type {
Expand Down
1 change: 1 addition & 0 deletions frontend/src/views/ReviewAlwaysRuleDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function ReviewAlwaysRuleDialog({ run, effect, onClose, onResolved }: {
await GuardrailService.CreateRule({
ID: '', Label: ruleName, Effect: (effect === 'allow' ? GuardrailEffect.EffectAllow : GuardrailEffect.EffectDeny), Condition: '', Source: '',
NodeTypeID: '', RequestID: '', WorkflowID: '', NodeID: '',
BuiltIn: false, Seed: { SeedRevision: 0, Modified: false },
...scopeFields,
})
} catch (err) {
Expand Down
Loading
Loading