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
2 changes: 0 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@
"test": "bun run build && bun test ./src"
},
"dependencies": {
"@corbits/approvals": "workspace:*",
"@corbits/credential-mcp": "^0.1.0",
"@corbits/error-sink": "workspace:*",
"@corbits/mcp": "^0.3.0",
"@corbits/react-ui": "0.3.0",
"@corbits/url-path": "workspace:*",
"@corbits/worker": "workspace:*",
"@corbits/workflows": "workspace:*",
"@intx/mime": "workspace:*",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test";
import { argumentsSummaryFor, headlineFor } from "./headline";
import { argumentsSummaryFor, headlineFor } from "./approval-headline";

// Like redact.test.ts, the provider-key fixtures here are stamped at runtime
// from benign parts so no full credential-shaped string exists contiguously at
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
// The human-readable line an approval is described by. Pure string work, no
// database dependency, so the browser can compose the same headline the
// hub-side tools do.
import { redactExtra, redactText } from "@corbits/error-sink";

function stringField(source: object, field: string): string | undefined {
Expand All @@ -25,9 +22,6 @@ function toolPackagePinsField(
);
}

/** Renders the packageName/toolPackagePins a prior `wf_deploy_preview` call
* reported, so the card names the real package instead of a bare asset id.
* Does not show grants/capabilities: those have no preview yet. */
function workflowDeployHeadline(toolArguments: object): string | undefined {
const commitSha = stringField(toolArguments, "commitSha");
if (commitSha === undefined) return undefined;
Expand All @@ -38,24 +32,15 @@ function workflowDeployHeadline(toolArguments: object): string | undefined {
const pins = toolPackagePinsField(toolArguments, "toolPackagePins");
const toolsText =
pins.length > 0 ? pins.map((pin) => `${pin.name}@${pin.version}`).join(", ") : "none declared";
// The package name, commit/sha, and any pin name/version are free-form caller
// input a secret could be smuggled into; redact the assembled line the same
// way write_file's content preview is, so a secret-shaped package or pin
// never reaches the headline.
return redactText(`Deploy workflow ${packageName} @ ${sha7} — tools: ${toolsText}`);
}

/** Per-tool headline renderers, keyed by tool name — registering here avoids
* another `if (toolName === ...)` branch in `headlineFor`. */
const TOOL_HEADLINE_RENDERERS: Readonly<
Record<string, (toolArguments: object) => string | undefined>
> = {
workflow_deploy: workflowDeployHeadline,
};

/** Builds the headline for an approval: a registered per-tool renderer wins
* when it can, else the tool's `description` (falling back to its bare
* `name`), with any call-supplied `title` appended. */
export function headlineFor(toolDefinition: unknown, toolArguments: unknown): string {
const toolName =
typeof toolDefinition === "object" && toolDefinition !== null
Expand All @@ -76,15 +61,9 @@ export function headlineFor(toolDefinition: unknown, toolArguments: unknown): st
typeof toolArguments === "object" && toolArguments !== null
? stringField(toolArguments, "title")
: undefined;
// The title is a generic free-form field a caller can stuff a secret into;
// redact it the same way generic arguments are, so a JSON string smuggled in
// the title goes through the same structural (key-named) redaction as
// `formatArgumentValue`, not just the flat assignment/keyword pass.
return title === undefined ? headline : `${headline}: "${formatArgumentValue(title)}"`;
}

/** The bare tool name off a toolDefinition snapshot, for a row that wants to
* name the call apart from its (possibly generic) description. */
export function toolNameFor(toolDefinition: unknown): string | undefined {
return typeof toolDefinition === "object" && toolDefinition !== null
? stringField(toolDefinition, "name")
Expand All @@ -100,28 +79,20 @@ function truncate(text: string, limit = ARGUMENT_PREVIEW_LIMIT): string {
function formatArgumentValue(value: unknown): string {
if (typeof value === "number" || typeof value === "boolean") return String(value);
if (value === null || value === undefined) return "";
// Default: redact a string (or a stringified object/array) structurally when
// it parses as JSON, so key-named secrets (`password`, `token`, ...) and
// nested JSON are caught; fall back to plain redactText for non-JSON strings.
if (typeof value === "string") {
try {
const parsed: unknown = JSON.parse(value);
if (parsed !== null && typeof parsed === "object") {
return JSON.stringify(redactExtra(parsed as Record<string, unknown>));
}
} catch {
// not JSON; redact as a plain string below
// Non-JSON string falls through to plain-text redaction.
}
return redactText(value);
}
// A live object/array argument: redact it structurally, then serialize.
const redacted = redactExtra(value as Record<string, unknown>);
return JSON.stringify(redacted);
return JSON.stringify(redactExtra(value as Record<string, unknown>));
}

// `write_file`'s call is read as its target path plus a preview of what it
// writes there -- the two facts a person approving it actually needs, not
// the raw argument bag.
function writeFileArgumentsSummary(toolArguments: object): string | undefined {
const path = stringField(toolArguments, "path");
if (path === undefined) return undefined;
Expand All @@ -137,10 +108,6 @@ const ARGUMENT_SUMMARY_RENDERERS: Readonly<
};

function genericArgumentsSummary(toolArguments: object): string | undefined {
// Redact the whole bag structurally first: this collapses key-named secrets
// (`password`, `token`, `apiKey`, ...) regardless of their value's shape and
// recurses into nested objects/strings, which the per-value format path
// below then renders (already-safe values pass through unchanged).
const redacted = redactExtra(toolArguments as Record<string, unknown>);
if (redacted === undefined) return undefined;
const entries = Object.entries(redacted);
Expand All @@ -150,10 +117,6 @@ function genericArgumentsSummary(toolArguments: object): string | undefined {
);
}

/** A compact, human-scannable rendering of a call's arguments -- a
* registered per-tool renderer wins when it can (e.g. `write_file`'s path +
* content preview), else a truncated `key: value` list. `undefined` when
* there is nothing to show, so a caller can render "tool name only". */
export function argumentsSummaryFor(
toolDefinition: unknown,
toolArguments: unknown,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/insights-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// insights-page.tsx) so it can be exercised directly without dragging in
// that page's bench/query-client wiring.

import { decodedOrNull } from "@corbits/url-path";
import { decodedOrNull } from "@/lib/url-path";

import { INSIGHTS_PATH_PREFIX, INSIGHTS_RUNS_PATH } from "./path-ids";

Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/library/kind-filter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Names here keep the "library" vocabulary (the internal name for the
// artifact store), but `LIBRARY_PATH` tracks the app's real mount point.

import { decodedOrNull } from "@corbits/url-path";
import { decodedOrNull } from "@/lib/url-path";

import { titleExtension } from "./title-extension";
import type { ArtifactSummary } from "./artifact-summary";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/path-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// out to their own top-level rail destinations (`/agents/:id`, `/skills/:id`).

import { isValidSlug, type Slug } from "@/lib/slug";
import { decodedOrNull } from "@corbits/url-path";
import { decodedOrNull } from "@/lib/url-path";

export const SETTINGS_PATH_PREFIX = "/settings";
export const WORKERS_PATH_PREFIX = "/workers";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/pending-approvals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { useQueries, useQuery } from "@tanstack/react-query";
import { type } from "arktype";

import type { APIQuery } from "@/lib/api-query";
import { argumentsSummaryFor, headlineFor, toolNameFor } from "@corbits/approvals/headline";

import { argumentsSummaryFor, headlineFor, toolNameFor } from "./chat/approval-headline";
import { listChatAgents } from "./chat/threads-api";
import { TenantApprovalsSchema, useAPIQuery } from "./api";
import { useBench } from "./bench-context";
Expand Down
2 changes: 1 addition & 1 deletion apps/web/src/workbench-path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// bookmarks land on the same surface instead of a dead route. Settings is
// a full stage surface, never a dialog, since a workbench is a tenant.

import { decodedOrNull } from "@corbits/url-path";
import { decodedOrNull } from "@/lib/url-path";

/** The settings sub-paths a workbench deep link still resolves. */
export const WORKBENCH_SETTINGS_SECTION_IDS = ["general", "members", "agents", "danger"] as const;
Expand Down
32 changes: 4 additions & 28 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading