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: 2 additions & 0 deletions cli/src/commands/fake-hook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface FakeHookOptions {
tool: string;
command?: string;
filePath?: string;
cwd?: string;
url?: string;
json: boolean;
inputJson?: string;
Expand All @@ -34,6 +35,7 @@ export async function runFakeHook(opts: FakeHookOptions): Promise<void> {
source: opts.source,
tool: opts.tool,
input,
cwd: opts.cwd,
};
const client = apiClient(opts.url);
let res: GateCheckResponse;
Expand Down
6 changes: 6 additions & 0 deletions cli/src/commands/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ interface Options {
policyHash?: string;
code?: string;
passphrase?: string;
userId?: string;
groups?: string[];
}

export async function runSessionEnd(opts: {
Expand Down Expand Up @@ -82,6 +84,8 @@ export async function runSessionRotate(opts: Options & { id: string }): Promise<
signer: payload.signer,
signer_pubkey: payload.signer_pubkey,
attestation: `ed25519:${toHex(attestation)}`,
user_id: opts.userId,
groups: opts.groups,
};

const client = apiClient(opts.url);
Expand Down Expand Up @@ -152,6 +156,8 @@ export async function runSessionCreate(opts: Options): Promise<void> {
signer: payload.signer,
signer_pubkey: payload.signer_pubkey,
attestation: `ed25519:${toHex(attestation)}`,
user_id: opts.userId,
groups: opts.groups,
};

const client = apiClient(opts.url);
Expand Down
15 changes: 15 additions & 0 deletions cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,8 @@ session
.option("--tier <tier>", "Signer tier (software | totp).", "software")
.option("--url <url>", "Control-plane base URL.")
.option("--policy-hash <hash>", "Policy hash to bind into the attestation.")
.option("--user-id <id>", "User identity used for group-policy overlays.")
.option("--group <name...>", "Group memberships used for group-policy overlays.")
.option("--code <6-digit>", "TOTP code (required for --tier totp).")
.option("--passphrase <pp>", "TOTP passphrase (required for --tier totp).")
.option("--json", "Emit JSON instead of human output.", false)
Expand All @@ -171,6 +173,8 @@ session
tier: string;
url?: string;
policyHash?: string;
userId?: string;
group?: string[];
code?: string;
passphrase?: string;
json: boolean;
Expand All @@ -197,6 +201,8 @@ session
url: opts.url,
json: opts.json,
policyHash: opts.policyHash,
userId: opts.userId,
groups: opts.group,
code: opts.code,
passphrase: opts.passphrase,
});
Expand All @@ -220,6 +226,8 @@ session
.option("--tier <tier>", "Signer tier (software | totp).", "software")
.option("--url <url>", "Control-plane base URL.")
.option("--policy-hash <hash>", "Policy hash to bind into the rotated attestation.")
.option("--user-id <id>", "User identity used for group-policy overlays.")
.option("--group <name...>", "Group memberships used for group-policy overlays.")
.option("--code <6-digit>", "TOTP code (required for --tier totp).")
.option("--passphrase <pp>", "TOTP passphrase (required for --tier totp).")
.option("--json", "Emit JSON instead of human output.", false)
Expand All @@ -229,6 +237,8 @@ session
tier: string;
url?: string;
policyHash?: string;
userId?: string;
group?: string[];
code?: string;
passphrase?: string;
json: boolean;
Expand All @@ -252,6 +262,8 @@ session
url: opts.url,
json: opts.json,
policyHash: opts.policyHash,
userId: opts.userId,
groups: opts.group,
code: opts.code,
passphrase: opts.passphrase,
});
Expand Down Expand Up @@ -362,6 +374,7 @@ program
.requiredOption("--tool <name>", "Tool name (Bash, Read, Write, mcp__X__Y).")
.option("--command <cmd>", "Bash command (shorthand for --input.command).")
.option("--file-path <path>", "File path (shorthand for --input.file_path).")
.option("--cwd <path>", "Working directory for scoped policy resolution.")
.option("--input <json>", "Raw tool input as JSON.")
.option("--url <url>", "Control-plane base URL.")
.option("--json", "Emit JSON instead of human output.", false)
Expand All @@ -372,6 +385,7 @@ program
tool: string;
command?: string;
filePath?: string;
cwd?: string;
input?: string;
url?: string;
json: boolean;
Expand All @@ -382,6 +396,7 @@ program
tool: opts.tool,
command: opts.command,
filePath: opts.filePath,
cwd: opts.cwd,
inputJson: opts.input,
url: opts.url,
json: opts.json,
Expand Down
5 changes: 5 additions & 0 deletions cli/src/util/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export interface PolicyGateView {
id: string;
mode?: string;
disabled?: boolean;
source?: string;
tool?: string;
tool_prefix?: string;
any_command_regex?: string[];
Expand Down Expand Up @@ -283,6 +284,8 @@ export interface SessionStartRequest {
signer: string;
signer_pubkey: string;
attestation: string;
user_id?: string;
groups?: string[];
}

export interface SessionResponse {
Expand All @@ -293,6 +296,8 @@ export interface SessionResponse {
session_pubkey: string;
signer: string;
signer_pubkey: string;
user_id?: string;
groups?: string[];
}

export function apiClient(baseUrl?: string, initialToken?: string | null): ApiClient {
Expand Down
69 changes: 69 additions & 0 deletions control-plane/api/group-policy.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://openagentlock.github.io/OpenAgentLock/schema/group-policy.schema.json",
"title": "OpenAgentLock group policy bundle",
"type": "object",
"required": ["version"],
"additionalProperties": false,
"properties": {
"version": { "type": "integer", "const": 1 },
"groups": {
"type": "object",
"additionalProperties": { "$ref": "#/definitions/policyLayer" }
},
"users": {
"type": "object",
"additionalProperties": {
"type": "object",
"additionalProperties": false,
"properties": {
"groups": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true
},
"gates": {
"type": "array",
"items": { "$ref": "#/definitions/gate" }
}
}
}
}
},
"definitions": {
"policyLayer": {
"type": "object",
"additionalProperties": false,
"properties": {
"inherits": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"uniqueItems": true
},
"gates": {
"type": "array",
"items": { "$ref": "#/definitions/gate" }
}
}
},
"gate": {
"type": "object",
"required": ["id", "match", "evaluate"],
"additionalProperties": true,
"properties": {
"id": { "type": "string", "minLength": 1 },
"source": { "type": "string" },
"mode": { "type": "string", "enum": ["monitor", "enforce"] },
"disabled": { "type": "boolean" },
"precedence": { "type": "string", "enum": ["priority"] },
"priority": { "type": "integer" },
"match": { "type": "object" },
"evaluate": {
"type": "array",
"minItems": 1,
"items": { "type": "object" }
}
}
}
}
}
4 changes: 4 additions & 0 deletions control-plane/api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,8 @@ components:
session_pubkey: { type: string }
signer: { $ref: '#/components/schemas/SignerKind' }
signer_pubkey: { type: string }
user_id: { type: string }
groups: { type: array, items: { type: string } }

SessionStartRequest:
type: object
Expand All @@ -278,6 +280,8 @@ components:
signer: { $ref: '#/components/schemas/SignerKind' }
signer_pubkey: { type: string }
attestation: { type: string, description: "ed25519 signature over canonical attestation payload" }
user_id: { type: string, description: "Optional identity key used for group-policy overlays." }
groups: { type: array, items: { type: string }, description: "Optional ordered group memberships used for group-policy overlays." }

GateCheckRequest:
type: object
Expand Down
10 changes: 10 additions & 0 deletions control-plane/dashboard-ui/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,22 @@ export interface LedgerEntry {
// mode forced the runtime to allow. UI renders these as "alert"
// (IDS-style) instead of "deny" (IPS-style) — the call did go through.
monitor_match?: boolean;
policy_trace?: PolicyTraceItem[];
payload_hash: string;
sig: string;
leaf_hash: string;
prev_leaf: string;
}

export interface PolicyTraceItem {
layer?: string;
source?: string;
rule_id: string;
verdict: string;
precedence?: string;
priority?: number;
}

export interface ModeInfo {
mode: "firewall" | "monitor";
env: string;
Expand Down
14 changes: 14 additions & 0 deletions control-plane/dashboard-ui/src/routes/events.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,20 @@ function EventDetail({
value="suppressed deny — runtime allowed; ledger keeps original verdict"
/>
)}
{entry.policy_trace && entry.policy_trace.length > 0 && (
<DetailRow
label="policy"
value={entry.policy_trace
.map((t) => {
const priority =
t.precedence === "priority" ? ` priority=${t.priority ?? 0}` : "";
return `${t.layer || t.source || "policy"}:${t.rule_id}=${t.verdict}${priority}`;
})
.join(" → ")}
mono
wrap
/>
)}
<DetailRow label="signer" value={entry.signer || "—"} mono />
<DetailRow label="tool_use_id" value={entry.tool_use_id || "—"} mono />
<DetailRow label="payload_hash" value={entry.payload_hash} mono wrap />
Expand Down
8 changes: 2 additions & 6 deletions control-plane/internal/api/gate.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"net/http"
"time"

"github.com/openagentlock/openagentlock/control-plane/internal/policy"
"github.com/openagentlock/openagentlock/control-plane/internal/storage"
)

Expand Down Expand Up @@ -66,15 +65,11 @@ func gateCheckHandler(d Deps) http.HandlerFunc {

// Resolve the policy pinned to this session's hash; falls back to
// live when the hash is unknown (e.g. registry not yet seeded).
evalPolicy := resolvePolicyForCwd(d, sess.PolicyHash, req.Cwd)
evalPolicy, result := evaluatePolicyForSession(d, sess, req.Cwd, req.Tool, req.Input)
if evalPolicy == nil {
writeError(w, http.StatusServiceUnavailable, "policy_unavailable", "no policy loaded")
return
}
result := evalPolicy.Evaluate(policy.EvalRequest{
Tool: req.Tool,
Input: req.Input,
})

var origVerdict string
result, _, origVerdict = applyDaemonModeOverride(result)
Expand Down Expand Up @@ -107,6 +102,7 @@ func gateCheckHandler(d Deps) http.HandlerFunc {
Verdict: origVerdict,
MonitorMatch: result.MonitorMatch,
MatcherInput: ledgerMatcherInput(req.Input),
PolicyTrace: storagePolicyTrace(result.Trace),
PayloadHash: payloadHash[:],
Sig: nil,
})
Expand Down
Loading
Loading