Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
cad9f16
feat(beltic): add houston-beltic crate — REST client, issuer, JWT-VC …
sajc11 May 22, 2026
e75918b
feat(engine-core): credentials module — persist Beltic VCs in .houston/
sajc11 May 22, 2026
45ae465
feat(engine-server): credentials + webhook routes; HoustonEvent::Cred…
sajc11 May 22, 2026
850d79c
chore: bump Cargo.lock after Beltic-related deps land
sajc11 May 22, 2026
0875eb5
feat(ui): TS types + engine-client methods + agent-credentials hooks
sajc11 May 22, 2026
0bf62e3
feat(app): Settings → Identity + Authorized agents sub-nav
sajc11 May 22, 2026
1e58567
feat(app): agent authorization consent dialog
sajc11 May 23, 2026
052ef3e
feat(app): Verified by Beltic tag on Mission Control cards
sajc11 May 23, 2026
7e2e6ec
feat: identity issuance route + verify modal + populated Identity pane
sajc11 May 23, 2026
4a14874
feat(app): wire delegated_by_subject_id to live identity; bake stagin…
sajc11 May 24, 2026
edbfa52
feat(engine): mint real ES256 did:jwk for agents on credential issuance
sajc11 May 24, 2026
2769416
fix(app): wire Identity + Authorized agents into Settings sidebar; fi…
sajc11 May 24, 2026
616948c
feat(app): identity verify dialog redesign + document evidence attach…
sajc11 May 25, 2026
9c430ab
feat(identity): persist attached evidence locally on submit
sajc11 May 25, 2026
82fcf7e
feat(identity): reveal-in-finder for attached evidence files
sajc11 May 25, 2026
d1555d0
feat(beltic): upload evidence to Beltic /v1/evidence; prefer evidence…
sajc11 May 25, 2026
8cf10c1
feat(identity): pre-select doc type before upload
sajc11 May 25, 2026
1d8b51c
docs(beltic): integration status + Beltic-side deploy dependencies
sajc11 May 25, 2026
bcfa646
chore: regenerate Cargo.lock (mime_guess transitive)
sajc11 May 25, 2026
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
302 changes: 300 additions & 2 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ members = [
"engine/houston-agents-conversations",
"engine/houston-file-watcher",
"engine/houston-composio",
"engine/houston-beltic",
"engine/houston-cli-bundle",
"engine/houston-claude-installer",
"engine/houston-engine-core",
Expand Down Expand Up @@ -46,6 +47,7 @@ houston-ui-events = { version = "0.4.12", path = "engine/houston-ui-events" }
houston-agents-conversations = { version = "0.4.12", path = "engine/houston-agents-conversations" }
houston-file-watcher = { version = "0.4.12", path = "engine/houston-file-watcher" }
houston-composio = { version = "0.4.12", path = "engine/houston-composio" }
houston-beltic = { version = "0.4.12", path = "engine/houston-beltic" }
houston-cli-bundle = { version = "0.4.12", path = "engine/houston-cli-bundle" }
houston-claude-installer = { version = "0.4.12", path = "engine/houston-claude-installer" }
houston-engine-core = { version = "0.4.12", path = "engine/houston-engine-core" }
Expand Down
32 changes: 32 additions & 0 deletions app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,38 @@ pub fn run() {
engine_env.push(("HOUSTON_TUNNEL_URL".into(), v));
}
}

// Beltic VC integration — debug-only staging defaults so
// `pnpm tauri dev` lights up the credentials flow without the
// dev needing to remember `export BELTIC_API_KEY=...` every
// session. Release builds NEVER see these literals (the
// cfg!(debug_assertions) gate strips them at compile time).
// Either env var, if set in the parent shell, wins over the
// default. Permissions on this key: credentials:read /
// :write / :revoke / :verify (no :delete) — matches what
// Houston's integration actually uses.
#[cfg(debug_assertions)]
{
if std::env::var("BELTIC_API_KEY").is_err() {
engine_env.push((
"BELTIC_API_KEY".into(),
"sk_staging_9YCYmYYTf0CEGz0zgCDxXiP4yIlLNiUw".into(),
));
}
if std::env::var("BELTIC_BASE_URL").is_err() {
engine_env.push((
"BELTIC_BASE_URL".into(),
"https://api.staging.beltic.com/v1".into(),
));
}
}
for var in ["BELTIC_API_KEY", "BELTIC_BASE_URL", "BELTIC_WEBHOOK_SECRET"] {
if let Ok(v) = std::env::var(var) {
if !v.is_empty() {
engine_env.push((var.into(), v));
}
}
}
// 30s banner timeout: first-run Gatekeeper scan on a notarized
// sidecar can take 15–20s on slow machines.
let slot = spawn_supervisor(binary, Duration::from_secs(30), engine_env, cb)
Expand Down
112 changes: 112 additions & 0 deletions app/src/components/settings/sections/agents-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";

import {
useActiveAgentCredential,
useRevokeAgentCredential,
} from "../../../hooks/queries/use-agent-credentials";
import { AuthorizeAgentDialog } from "./authorize-agent-dialog";

interface Props {
agentId: string;
agentName: string;
agentPath: string;
}

/** One row in the Authorized agents list. */
export function AgentCredentialsRow({ agentId, agentName, agentPath }: Props) {
const { t } = useTranslation("settings");
const credential = useActiveAgentCredential(agentPath);
const revoke = useRevokeAgentCredential(agentPath);
const [authOpen, setAuthOpen] = useState(false);

const shortId = useMemo(() => {
if (!credential) return null;
const id = credential.credential_id;
if (id.length <= 18) return id;
return `${id.slice(0, 9)}…${id.slice(-6)}`;
}, [credential]);

return (
<li className="flex items-center gap-4 px-5 py-4">
<div className="flex-1 min-w-0">
<div className="text-sm font-medium truncate">{agentName}</div>
<div className="text-xs text-muted-foreground truncate">
{credential ? (
<>
<code className="font-mono">{shortId}</code>
{credential.delegated_by_subject_id ? (
<span>
{" · "}
{t("agents.columnDelegation")}
{": "}
<code className="font-mono">
{credential.delegated_by_subject_id}
</code>
</span>
) : null}
</>
) : (
<span className="italic">{t("agents.noCredential")}</span>
)}
</div>
</div>

{credential ? <StatusPill status={credential.status} /> : null}

{credential && credential.status === "active" ? (
<button
type="button"
className="rounded-full border border-black/15 px-3 h-8 text-xs font-medium hover:bg-gray-50 disabled:opacity-50"
disabled={revoke.isPending}
onClick={() => {
if (window.confirm(t("agents.revokeConfirm"))) {
revoke.mutate(credential.credential_id);
}
}}
>
{t("agents.revoke")}
</button>
) : (
<button
type="button"
className="rounded-full bg-gray-950 text-white px-3 h-8 text-xs font-medium hover:bg-gray-800"
onClick={() => setAuthOpen(true)}
>
{t("agents.authorize")}
</button>
)}

<AuthorizeAgentDialog
agentId={agentId}
agentName={agentName}
agentPath={agentPath}
open={authOpen}
onOpenChange={setAuthOpen}
/>
</li>
);
}

function StatusPill({ status }: { status: string }) {
const { t } = useTranslation("settings");
const label =
status === "active"
? t("identity.statusVerified")
: status === "revoked"
? t("identity.statusRevoked")
: status;
const tone =
status === "active"
? "bg-emerald-50 text-emerald-700 border-emerald-200"
: status === "revoked"
? "bg-red-50 text-red-700 border-red-200"
: "bg-gray-100 text-gray-700 border-gray-300";
return (
<span
className={`text-xs font-medium rounded-full border px-2.5 h-6 inline-flex items-center ${tone}`}
>
{label}
</span>
);
}
53 changes: 53 additions & 0 deletions app/src/components/settings/sections/agents.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { useTranslation } from "react-i18next";
import { useAgentStore } from "../../../stores/agents";
import { AgentCredentialsRow } from "./agents-row";

/**
* Settings → Authorized agents. Lists every agent in the current
* workspace plus its Beltic `agent_authorization` credential status,
* delegation chain (the user credential that authorized it), and a
* revoke action.
*
* Each row fetches its own credentials list via TanStack Query. The WS
* event invalidator (chunk 4) auto-refreshes when the engine emits
* CredentialIssued/Revoked/Suspended.
*/
export function AgentsSection() {
const { t } = useTranslation("settings");
const agents = useAgentStore((s) => s.agents);

if (agents.length === 0) {
return (
<section className="space-y-6">
<header>
<h2 className="text-lg font-semibold mb-1">{t("agents.title")}</h2>
<p className="text-sm text-muted-foreground">{t("agents.subtitle")}</p>
</header>
<div className="rounded-xl border border-border bg-card p-6 space-y-1 text-sm">
<h3 className="text-base font-semibold">{t("agents.emptyTitle")}</h3>
<p className="text-muted-foreground">{t("agents.emptyDescription")}</p>
</div>
</section>
);
}

return (
<section className="space-y-6">
<header>
<h2 className="text-lg font-semibold mb-1">{t("agents.title")}</h2>
<p className="text-sm text-muted-foreground">{t("agents.subtitle")}</p>
</header>

<ul className="divide-y divide-border rounded-xl border border-border bg-card overflow-hidden">
{agents.map((agent) => (
<AgentCredentialsRow
key={agent.id}
agentId={agent.id}
agentName={agent.name}
agentPath={agent.folderPath}
/>
))}
</ul>
</section>
);
}
Loading