@aryaminus/controlkeel-opencode 0.4.1 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/commands/controlkeel-last.md +1 -1
- package/.opencode/commands/controlkeel-submit-plan.md +8 -5
- package/.opencode/plugins/controlkeel-governance.ts +65 -7
- package/.opencode/skills/compliance-audit/SKILL.md +11 -15
- package/.opencode/skills/domain-audit/SKILL.md +12 -23
- package/.opencode/skills/end-of-shift/SKILL.md +8 -4
- package/.opencode/skills/handoff/SKILL.md +11 -4
- package/.opencode/skills/parallel-review/SKILL.md +5 -2
- package/.opencode/skills/security-review/SKILL.md +8 -3
- package/index.js +65 -7
- package/package.json +1 -1
|
@@ -5,4 +5,4 @@ Use this command in OpenCode to reopen the latest ControlKeel review you are tra
|
|
|
5
5
|
Suggested flow:
|
|
6
6
|
1. Read the last stored review id from your notes or prior command output.
|
|
7
7
|
2. Run `controlkeel review plan open --id <review_id> --json`
|
|
8
|
-
3. If the review is still pending,
|
|
8
|
+
3. If the review is still pending, ask the user for approval in this conversation, then record it with `controlkeel review plan respond <review_id> --decision approved --json`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: Submit the current plan to ControlKeel
|
|
2
|
+
description: Submit the current plan to ControlKeel for approval
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
Save the current plan to a markdown file, then submit it through ControlKeel.
|
|
@@ -8,10 +8,13 @@ Recommended flow:
|
|
|
8
8
|
1. Save the plan to `.opencode/review-plan.md`
|
|
9
9
|
2. Ensure `controlkeel version` reports `>= 0.1.26`
|
|
10
10
|
3. Run `controlkeel review plan submit --body-file .opencode/review-plan.md --submitted-by opencode --task-id <task_id> --json` (or use `--session-id <session_id>`)
|
|
11
|
-
4. Read the returned `review.id` and `
|
|
12
|
-
5. If `
|
|
13
|
-
6.
|
|
14
|
-
7.
|
|
11
|
+
4. Read the returned `review.id`, `browser_url`, and `auto_approved`
|
|
12
|
+
5. If `auto_approved` is true, the plan was approved automatically (low risk, no blocked findings). Proceed.
|
|
13
|
+
6. Otherwise, present the plan summary to the user and ask for approval in this conversation
|
|
14
|
+
7. After the user approves, record it with `controlkeel review plan respond <review_id> --decision approved --feedback-notes "User approved in chat" --json` (or `ck_review_feedback`)
|
|
15
|
+
8. Do not execute until the review is approved
|
|
16
|
+
|
|
17
|
+
Auto-approval: For low-risk work (small scope, no security concerns, within established patterns), the server may auto-approve. Check `auto_approved` in the response.
|
|
15
18
|
|
|
16
19
|
Fallback when the `submit_plan` tool is stale in a long-running OpenCode session:
|
|
17
20
|
- If the tool returns an error like `ControlKeel CLI [object Object] is too old`, run the CLI flow above directly.
|
|
@@ -68,6 +68,25 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
// CLI commands emit a `{command, data, status, version}` envelope; unwrap it so
|
|
72
|
+
// callers can read `review.id`, `session_id`, `browser_url` at the top level.
|
|
73
|
+
// Older flat payloads pass through unchanged.
|
|
74
|
+
const parseCliJson = (output: string) => {
|
|
75
|
+
const payload = parseJson(output)
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
payload != null &&
|
|
79
|
+
typeof payload === "object" &&
|
|
80
|
+
!Array.isArray(payload) &&
|
|
81
|
+
payload.data != null &&
|
|
82
|
+
typeof payload.data === "object"
|
|
83
|
+
) {
|
|
84
|
+
return payload.data
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return payload
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
const toText = async (output: unknown) => {
|
|
72
91
|
if (typeof output === "string") {
|
|
73
92
|
return output
|
|
@@ -242,7 +261,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
242
261
|
)
|
|
243
262
|
}
|
|
244
263
|
|
|
245
|
-
const contextPayload =
|
|
264
|
+
const contextPayload = parseCliJson([contextOut, contextErr].filter(Boolean).join("\n"))
|
|
246
265
|
const contextTaskId = contextPayload?.current_task?.id
|
|
247
266
|
const contextSessionId = contextPayload?.session_id
|
|
248
267
|
|
|
@@ -316,7 +335,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
316
335
|
)
|
|
317
336
|
}
|
|
318
337
|
|
|
319
|
-
const submitPayload =
|
|
338
|
+
const submitPayload = parseCliJson([submitOut, submitErr].filter(Boolean).join("\n"))
|
|
320
339
|
|
|
321
340
|
if (typeof submitPayload?.error === "string" && submitPayload.error.includes("session_id")) {
|
|
322
341
|
throw new Error(
|
|
@@ -346,7 +365,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
346
365
|
const openExit = await openProc.exited
|
|
347
366
|
|
|
348
367
|
if (openExit === 0) {
|
|
349
|
-
openPayload =
|
|
368
|
+
openPayload = parseCliJson([openOut, openErr].filter(Boolean).join("\n"))
|
|
350
369
|
} else {
|
|
351
370
|
openPayload = {
|
|
352
371
|
error: `controlkeel review plan open failed with exit code ${openExit}${openErr.trim() ? `: ${openErr.trim()}` : ""}`,
|
|
@@ -377,6 +396,22 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
377
396
|
guidance: overrides.guidance ?? null,
|
|
378
397
|
})
|
|
379
398
|
|
|
399
|
+
// If the server auto-approved, skip the entire open/wait flow
|
|
400
|
+
const autoApproved = submitPayload?.auto_approved === true
|
|
401
|
+
const autoApproveReason = submitPayload?.auto_approve_reason ?? null
|
|
402
|
+
|
|
403
|
+
if (autoApproved) {
|
|
404
|
+
return buildPlanResult({
|
|
405
|
+
status: "approved",
|
|
406
|
+
waitSkipped: true,
|
|
407
|
+
manualApprovalRequired: false,
|
|
408
|
+
reason: "auto_approved",
|
|
409
|
+
guidance: autoApproveReason
|
|
410
|
+
? `Plan auto-approved: ${autoApproveReason}`
|
|
411
|
+
: "Plan was auto-approved by ControlKeel (low risk, no blocked findings, within policy).",
|
|
412
|
+
})
|
|
413
|
+
}
|
|
414
|
+
|
|
380
415
|
const openError = typeof openPayload?.open_error === "string" ? openPayload.open_error.trim() : ""
|
|
381
416
|
const openFailure = typeof openPayload?.error === "string" ? openPayload.error.trim() : ""
|
|
382
417
|
const browserNotOpened = openPayload?.opened !== true
|
|
@@ -387,7 +422,11 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
387
422
|
browserUrl.includes("localhost") &&
|
|
388
423
|
openPayload?.remote === true
|
|
389
424
|
|
|
390
|
-
|
|
425
|
+
// Default: always return inline approval guidance.
|
|
426
|
+
const browserAvailable =
|
|
427
|
+
browserUrl && !serverUnavailable && !openError && !openFailure && !remoteLocalhostMismatch && !browserNotOpened
|
|
428
|
+
|
|
429
|
+
if (!browserAvailable) {
|
|
391
430
|
return buildPlanResult({
|
|
392
431
|
waitSkipped: true,
|
|
393
432
|
manualApprovalRequired: true,
|
|
@@ -400,10 +439,29 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
400
439
|
? "browser_not_opened"
|
|
401
440
|
: "browser_unreachable",
|
|
402
441
|
guidance:
|
|
403
|
-
"
|
|
442
|
+
"Ask the user for explicit approval in this conversation. " +
|
|
443
|
+
"Present the plan summary and ask: 'Do you approve this plan? (yes/no)'. " +
|
|
444
|
+
"After the user says yes, record it with: `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat\" --json` " +
|
|
445
|
+
"or call `ck_review_feedback` with review_id=<review_id> decision=\"approved\".",
|
|
404
446
|
})
|
|
405
447
|
}
|
|
406
448
|
|
|
449
|
+
// Browser is available. If the caller explicitly passed wait_timeout_seconds,
|
|
450
|
+
// honor it (opt-in blocking). Otherwise, return immediately with inline guidance.
|
|
451
|
+
if (waitTimeoutSeconds == null) {
|
|
452
|
+
return buildPlanResult({
|
|
453
|
+
waitSkipped: true,
|
|
454
|
+
manualApprovalRequired: false,
|
|
455
|
+
reason: "browser_available_inline_default",
|
|
456
|
+
guidance:
|
|
457
|
+
"Browser review is available at: " + browserUrl + "\n" +
|
|
458
|
+
"You can: (a) ask the user to approve inline in this conversation, or (b) pass wait_timeout_seconds to block until the browser review is completed. " +
|
|
459
|
+
"To record inline approval: `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat\" --json`.",
|
|
460
|
+
})
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Opt-in blocking: caller explicitly asked to wait for browser review.
|
|
464
|
+
|
|
407
465
|
const waitEnv = process.env.LOGGER_LEVEL
|
|
408
466
|
? process.env
|
|
409
467
|
: { ...process.env, LOGGER_LEVEL: "warning" }
|
|
@@ -416,7 +474,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
416
474
|
const waitOut = await new Response(waitProc.stdout).text()
|
|
417
475
|
const waitErr = await new Response(waitProc.stderr).text()
|
|
418
476
|
const waitExit = await waitProc.exited
|
|
419
|
-
const waitPayload =
|
|
477
|
+
const waitPayload = parseCliJson([waitOut, waitErr].filter(Boolean).join("\n"))
|
|
420
478
|
const waitMessage = typeof waitPayload?.message === "string" ? waitPayload.message.toLowerCase() : ""
|
|
421
479
|
const waitError = typeof waitPayload?.error === "string" ? waitPayload.error.toLowerCase() : ""
|
|
422
480
|
const waitTimedOut = waitMessage.includes("timeout") || waitError.includes("timed out")
|
|
@@ -433,7 +491,7 @@ export const ControlKeelGovernance: Plugin = async ({ project, client, $, direct
|
|
|
433
491
|
manualApprovalRequired: true,
|
|
434
492
|
reason: "review_timeout",
|
|
435
493
|
guidance:
|
|
436
|
-
"Plan review is still pending after timeout. Show the `browser_url` to the user if reachable. If browser review is unavailable or the user explicitly approves in chat, record it with `controlkeel review plan respond
|
|
494
|
+
"Plan review is still pending after timeout. Show the `browser_url` to the user if reachable. If browser review is unavailable or the user explicitly approves in chat, record it with `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat after timeout/browser issue\" --json` (or `ck_review_feedback`) before proceeding.",
|
|
437
495
|
})
|
|
438
496
|
}
|
|
439
497
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: compliance-audit
|
|
3
|
-
description: "
|
|
4
|
-
when_to_use: "
|
|
5
|
-
argument-hint: "[
|
|
3
|
+
description: "DEPRECATED — use `security-review` instead (it now covers all of this — regulated flows, policy packs, domain controls). Kept as a thin alias for search (compliance/GDPR/SOC2/HIPAA) so existing `ck_skill_list` queries still surface `security-review`."
|
|
4
|
+
when_to_use: "Do NOT invoke this skill directly — invoke `security-review`, which runs the compliance audit as its policy/domain-pack layer. This file exists only so keyword search for compliance, GDPR, SOC2, HIPAA routes to the consolidated `security-review` path."
|
|
5
|
+
argument-hint: "[use security-review instead]"
|
|
6
6
|
license: Apache-2.0
|
|
7
|
+
redirect_to: security-review
|
|
7
8
|
compatibility:
|
|
8
9
|
- codex
|
|
9
10
|
- claude-standalone
|
|
@@ -40,18 +41,13 @@ metadata:
|
|
|
40
41
|
- ck_context
|
|
41
42
|
- ck_validate
|
|
42
43
|
- ck_finding
|
|
44
|
+
deprecated: true
|
|
45
|
+
superseded_by: security-review
|
|
43
46
|
---
|
|
44
47
|
|
|
45
|
-
# Compliance Audit Skill
|
|
48
|
+
# Compliance Audit Skill — alias
|
|
46
49
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
3. Use `ck_validate` for concrete snippets and configs when the checklist points to code.
|
|
52
|
-
4. Persist each failing control with `ck_finding`.
|
|
53
|
-
5. End with packs checked, controls reviewed, blockers, and required approvals.
|
|
54
|
-
|
|
55
|
-
## Additional resources
|
|
56
|
-
|
|
57
|
-
- For the full domain-by-domain checklist, see [references/control-matrix.md](references/control-matrix.md)
|
|
50
|
+
> **This skill has moved.** Use `security-review` — it runs the same compliance audit
|
|
51
|
+
> as its policy/domain-pack layer (active compliance profile → pack sections →
|
|
52
|
+
> `ck_validate` → `ck_finding`). This file is a thin keyword alias so search for
|
|
53
|
+
> `compliance`, `GDPR`, `SOC2`, `HIPAA` still surfaces the consolidated path.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: domain-audit
|
|
3
|
-
description: "
|
|
4
|
-
when_to_use: "
|
|
5
|
-
argument-hint: "[
|
|
3
|
+
description: "DEPRECATED — use `security-review` instead (it now covers all domain-pack audits — HR, legal, marketing, sales, real-estate, government, insurance, logistics, manufacturing, e-commerce, nonprofit). Kept as a thin alias so `ck_skill_list` for those domains still surfaces `security-review`."
|
|
4
|
+
when_to_use: "Do NOT invoke this skill directly — invoke `security-review`, which runs the domain-pack audit as its policy/domain-pack layer. This file exists only so keyword search for HR, legal, marketing, sales, government, etc. routes to the consolidated `security-review` path."
|
|
5
|
+
argument-hint: "[use security-review instead]"
|
|
6
6
|
license: Apache-2.0
|
|
7
|
+
redirect_to: security-review
|
|
7
8
|
compatibility:
|
|
8
9
|
- codex
|
|
9
10
|
- claude-standalone
|
|
@@ -39,26 +40,14 @@ metadata:
|
|
|
39
40
|
ck_mcp_tools:
|
|
40
41
|
- ck_context
|
|
41
42
|
- ck_finding
|
|
43
|
+
deprecated: true
|
|
44
|
+
superseded_by: security-review
|
|
42
45
|
---
|
|
43
46
|
|
|
44
|
-
# Domain Audit Skill
|
|
47
|
+
# Domain Audit Skill — alias
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
- Legal: privilege, retention, document handling
|
|
52
|
-
- Marketing: consent, unsubscribe, analytics PII
|
|
53
|
-
- Sales: CRM data, contact deletion, revenue visibility
|
|
54
|
-
- Real estate: fair-housing logic, tenant PII, retention
|
|
55
|
-
- Government: records retention, constituent data, approval chains
|
|
56
|
-
- Insurance: claims fairness, medical-adjacent privacy, denial review
|
|
57
|
-
- E-commerce: card scope, refunds, fraud controls
|
|
58
|
-
- Logistics: shipment custody, dispatch safety, carrier data
|
|
59
|
-
- Manufacturing: QA holds, traceability, plant safety
|
|
60
|
-
- Nonprofit: donor privacy, grant restrictions, beneficiary exports
|
|
61
|
-
|
|
62
|
-
## Additional resources
|
|
63
|
-
|
|
64
|
-
- [Domain review matrix](references/domain-review-matrix.md)
|
|
49
|
+
> **This skill has moved.** Use `security-review` — it runs the same domain-pack
|
|
50
|
+
> audit as its policy/domain-pack layer. This file is a thin keyword alias so
|
|
51
|
+
> search for `HR`, `legal`, `marketing`, `sales`, `government`, `insurance`,
|
|
52
|
+
> `logistics`, `manufacturing`, `e-commerce`, or `nonprofit` still surfaces
|
|
53
|
+
> the consolidated path.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: end-of-shift
|
|
3
|
-
description: "Close a governed work session
|
|
4
|
-
when_to_use: "Activate
|
|
3
|
+
description: "Close a governed work session — the unified session-close path (validation + proof + findings + budget + digest + learning) that ends with the `handoff` delegation step when work remains. Prefer this over bare `handoff` for any end-of-shift wrap-up. Use when the user asks to wrap up, stop for the day, or leave work ready for another agent."
|
|
4
|
+
when_to_use: "Activate at any explicit session stop point (wrap up, stop for the day, leave work for another agent). It subsumes the bare `handoff` flow — `handoff` is just this skill's final delegation step, kept callable alone for mid-session delegation. Activate only at for end-of-shift validation. Do not run expensive full-suite or benchmark work unless project policy or the approved plan requires it."
|
|
5
5
|
argument-hint: "[completed work or remaining task]"
|
|
6
6
|
disable-model-invocation: true
|
|
7
7
|
license: Apache-2.0
|
|
@@ -27,8 +27,10 @@ result-schema:
|
|
|
27
27
|
handoff_reference: {type: [string, "null"]}
|
|
28
28
|
metadata:
|
|
29
29
|
author: controlkeel
|
|
30
|
-
version: "1.
|
|
30
|
+
version: "1.1"
|
|
31
31
|
category: execution
|
|
32
|
+
supersedes: handoff
|
|
33
|
+
subsumes: handoff
|
|
32
34
|
ck_mcp_tools:
|
|
33
35
|
- ck_context
|
|
34
36
|
- ck_git_status
|
|
@@ -42,7 +44,9 @@ metadata:
|
|
|
42
44
|
- ck_checkpoint_create
|
|
43
45
|
---
|
|
44
46
|
|
|
45
|
-
# End of Shift
|
|
47
|
+
# End of Shift — unified session-close
|
|
48
|
+
|
|
49
|
+
> `handoff` is step 7 of this skill (mid-session delegation without the earlier validation/proof/digest/learning gates). Use bare `handoff` only for mid-session delegation; this skill is the full close — it validates, finalizes proof, checks budget, synthesizes learnings, then hands off if work remains.
|
|
46
50
|
|
|
47
51
|
Close work with enough verified state that the next human or agent can continue without reconstructing the session from chat.
|
|
48
52
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: handoff
|
|
3
|
-
description: "Persist session state and hand off in-progress work to a background agent
|
|
4
|
-
when_to_use: "Activate
|
|
3
|
+
description: "Persist session state and hand off in-progress work to a background agent — the *delegation step* of `end-of-shift`. Use when work outgrows the current session or must continue unattended; for a full session close (validation + proof + findings + budget + digest + learning before handoff) prefer `end-of-shift`."
|
|
4
|
+
when_to_use: "Activate ONLY for mid-session delegation (context near limit, `ck_route` recommends a different agent/runtime, user says hand off/delegate/pass off). For an end-of-shift wrap-up (validate remaining work, finalize proofs, synthesize learnings), use `end-of-shift` instead — `handoff` is that skill's final step."
|
|
5
5
|
argument-hint: "[optional: specific task or remaining work to hand off]"
|
|
6
6
|
disable-model-invocation: true
|
|
7
7
|
license: Apache-2.0
|
|
8
|
+
redirect_to: end-of-shift
|
|
8
9
|
compatibility:
|
|
9
10
|
- codex
|
|
10
11
|
- claude-standalone
|
|
@@ -35,8 +36,9 @@ compatibility:
|
|
|
35
36
|
- forge-acp
|
|
36
37
|
metadata:
|
|
37
38
|
author: controlkeel
|
|
38
|
-
version: "1.
|
|
39
|
+
version: "1.1"
|
|
39
40
|
category: execution
|
|
41
|
+
superseded_by: end-of-shift
|
|
40
42
|
ck_mcp_tools:
|
|
41
43
|
- ck_context
|
|
42
44
|
- ck_memory_record
|
|
@@ -46,7 +48,12 @@ metadata:
|
|
|
46
48
|
- ck_goal
|
|
47
49
|
---
|
|
48
50
|
|
|
49
|
-
# Handoff Skill
|
|
51
|
+
# Handoff Skill — delegation step
|
|
52
|
+
|
|
53
|
+
> This skill is the **delegation sub-step** of `end-of-shift`. A full end-of-shift
|
|
54
|
+
> run validates remaining work, finalizes proofs, records digests/learnings, and THEN
|
|
55
|
+
> hands off. Use this skill alone only for mid-session delegation; otherwise prefer
|
|
56
|
+
> `end-of-shift`.
|
|
50
57
|
|
|
51
58
|
Transfer in-progress work to another agent or execution context with full state preservation, so work continues seamlessly after the current session ends or context runs out.
|
|
52
59
|
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: parallel-review
|
|
3
|
-
description: "
|
|
4
|
-
when_to_use: "
|
|
3
|
+
description: "DEPRECATED for inline use — this is now a CLI composite (`controlkeel review --parallel`). Kept callable only for explicitly requested comprehensive security+quality orchestration; otherwise invoke security-review + deep-code-quality-review separately or via that CLI."
|
|
4
|
+
when_to_use: "Do NOT invoke via skills when possible — use `controlkeel review --parallel` (CLI). Invoke this skill only when explicitly asked for comprehensive security+quality orchestration."
|
|
5
|
+
|
|
5
6
|
argument-hint: "[PR, branch, or diff]"
|
|
6
7
|
disable-model-invocation: true
|
|
8
|
+
deprecated: true
|
|
9
|
+
prefer_cli: "controlkeel review --parallel"
|
|
7
10
|
license: Apache-2.0
|
|
8
11
|
compatibility:
|
|
9
12
|
- opencode-native
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: security-review
|
|
3
|
-
description: "Run a structured security review before marking a task done. Use this for code, config, architecture, or release reviews
|
|
4
|
-
when_to_use: "Use before merging, deploying, or signing off on code, config, or architecture changes
|
|
5
|
-
argument-hint: "[file, PR, or
|
|
3
|
+
description: "Run a structured security review before marking a task done. Services as the single audit skill for OWASP, baseline pack, domain-pack (HR/legal/marketing/sales/real-estate/government/insurance/logistics/manufacturing/e-commerce/nonprofit), and compliance (GDPR/SOC2/HIPAA/policy packs) — subsumes the deprecated `compliance-audit` and `domain-audit` aliases. Use this for code, config, architecture, or release reviews; the domain/compliance layer is triggered by the session's active domain pack and data flows."
|
|
4
|
+
when_to_use: "Use before merging, deploying, or signing off on code, config, or architecture changes (including regulated data flows, external integrations, exports, and any session whose domain pack is HR/legal/marketing/sales/real-estate/government/insurance/logistics/manufacturing/e-commerce/nonprofit). Activates for auth/input/secrets/deps and for domain/compliance coverage — the policy/domain-pack layer is driven by `ck_context`'s active compliance profile."
|
|
5
|
+
argument-hint: "[file, PR, area, or domain-pack scope to review]"
|
|
6
6
|
license: Apache-2.0
|
|
7
|
+
redirect_to: security-review
|
|
7
8
|
compatibility:
|
|
8
9
|
- codex
|
|
9
10
|
- claude-standalone
|
|
@@ -36,6 +37,10 @@ metadata:
|
|
|
36
37
|
author: controlkeel
|
|
37
38
|
version: "2.1"
|
|
38
39
|
category: security
|
|
40
|
+
subsumes:
|
|
41
|
+
- compliance-audit
|
|
42
|
+
- domain-audit
|
|
43
|
+
- agent-pattern-verification
|
|
39
44
|
ck_mcp_tools:
|
|
40
45
|
- ck_validate
|
|
41
46
|
- ck_context
|
package/index.js
CHANGED
|
@@ -64,6 +64,25 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// CLI commands emit a `{command, data, status, version}` envelope; unwrap it so
|
|
68
|
+
// callers can read `review.id`, `session_id`, `browser_url` at the top level.
|
|
69
|
+
// Older flat payloads pass through unchanged.
|
|
70
|
+
const parseCliJson = (output) => {
|
|
71
|
+
const payload = parseJson(output)
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
payload != null &&
|
|
75
|
+
typeof payload === "object" &&
|
|
76
|
+
!Array.isArray(payload) &&
|
|
77
|
+
payload.data != null &&
|
|
78
|
+
typeof payload.data === "object"
|
|
79
|
+
) {
|
|
80
|
+
return payload.data
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return payload
|
|
84
|
+
}
|
|
85
|
+
|
|
67
86
|
const toText = async (output) => {
|
|
68
87
|
if (typeof output === "string") {
|
|
69
88
|
return output
|
|
@@ -232,7 +251,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
232
251
|
)
|
|
233
252
|
}
|
|
234
253
|
|
|
235
|
-
const contextPayload =
|
|
254
|
+
const contextPayload = parseCliJson([contextOut, contextErr].filter(Boolean).join("\n"))
|
|
236
255
|
const contextTaskId = contextPayload?.current_task?.id
|
|
237
256
|
const contextSessionId = contextPayload?.session_id
|
|
238
257
|
|
|
@@ -299,7 +318,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
299
318
|
)
|
|
300
319
|
}
|
|
301
320
|
|
|
302
|
-
const submitPayload =
|
|
321
|
+
const submitPayload = parseCliJson([submitOut, submitErr].filter(Boolean).join("\n"))
|
|
303
322
|
|
|
304
323
|
if (typeof submitPayload?.error === "string" && submitPayload.error.includes("session_id")) {
|
|
305
324
|
throw new Error(
|
|
@@ -329,7 +348,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
329
348
|
const openExit = await openProc.exited
|
|
330
349
|
|
|
331
350
|
if (openExit === 0) {
|
|
332
|
-
openPayload =
|
|
351
|
+
openPayload = parseCliJson([openOut, openErr].filter(Boolean).join("\n"))
|
|
333
352
|
} else {
|
|
334
353
|
openPayload = {
|
|
335
354
|
error: `controlkeel review plan open failed with exit code ${openExit}${openErr.trim() ? `: ${openErr.trim()}` : ""}`,
|
|
@@ -360,6 +379,22 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
360
379
|
guidance: overrides.guidance ?? null,
|
|
361
380
|
})
|
|
362
381
|
|
|
382
|
+
// If the server auto-approved, skip the entire open/wait flow
|
|
383
|
+
const autoApproved = submitPayload?.auto_approved === true
|
|
384
|
+
const autoApproveReason = submitPayload?.auto_approve_reason ?? null
|
|
385
|
+
|
|
386
|
+
if (autoApproved) {
|
|
387
|
+
return buildPlanResult({
|
|
388
|
+
status: "approved",
|
|
389
|
+
waitSkipped: true,
|
|
390
|
+
manualApprovalRequired: false,
|
|
391
|
+
reason: "auto_approved",
|
|
392
|
+
guidance: autoApproveReason
|
|
393
|
+
? `Plan auto-approved: ${autoApproveReason}`
|
|
394
|
+
: "Plan was auto-approved by ControlKeel (low risk, no blocked findings, within policy).",
|
|
395
|
+
})
|
|
396
|
+
}
|
|
397
|
+
|
|
363
398
|
const openError = typeof openPayload?.open_error === "string" ? openPayload.open_error.trim() : ""
|
|
364
399
|
const openFailure = typeof openPayload?.error === "string" ? openPayload.error.trim() : ""
|
|
365
400
|
const browserNotOpened = openPayload?.opened !== true
|
|
@@ -370,7 +405,11 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
370
405
|
browserUrl.includes("localhost") &&
|
|
371
406
|
openPayload?.remote === true
|
|
372
407
|
|
|
373
|
-
|
|
408
|
+
// Default: always return inline approval guidance.
|
|
409
|
+
const browserAvailable =
|
|
410
|
+
browserUrl && !serverUnavailable && !openError && !openFailure && !remoteLocalhostMismatch && !browserNotOpened
|
|
411
|
+
|
|
412
|
+
if (!browserAvailable) {
|
|
374
413
|
return buildPlanResult({
|
|
375
414
|
waitSkipped: true,
|
|
376
415
|
manualApprovalRequired: true,
|
|
@@ -383,10 +422,29 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
383
422
|
? "browser_not_opened"
|
|
384
423
|
: "browser_unreachable",
|
|
385
424
|
guidance:
|
|
386
|
-
"
|
|
425
|
+
"Ask the user for explicit approval in this conversation. " +
|
|
426
|
+
"Present the plan summary and ask: 'Do you approve this plan? (yes/no)'. " +
|
|
427
|
+
"After the user says yes, record it with: `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat\" --json` " +
|
|
428
|
+
"or call `ck_review_feedback` with review_id=<review_id> decision=\"approved\".",
|
|
387
429
|
})
|
|
388
430
|
}
|
|
389
431
|
|
|
432
|
+
// Browser is available. If the caller explicitly passed wait_timeout_seconds,
|
|
433
|
+
// honor it (opt-in blocking). Otherwise, return immediately with inline guidance.
|
|
434
|
+
if (waitTimeoutSeconds == null) {
|
|
435
|
+
return buildPlanResult({
|
|
436
|
+
waitSkipped: true,
|
|
437
|
+
manualApprovalRequired: false,
|
|
438
|
+
reason: "browser_available_inline_default",
|
|
439
|
+
guidance:
|
|
440
|
+
"Browser review is available at: " + browserUrl + "\n" +
|
|
441
|
+
"You can: (a) ask the user to approve inline in this conversation, or (b) pass wait_timeout_seconds to block until the browser review is completed. " +
|
|
442
|
+
"To record inline approval: `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat\" --json`.",
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Opt-in blocking: caller explicitly asked to wait for browser review.
|
|
447
|
+
|
|
390
448
|
const waitEnv = process.env.LOGGER_LEVEL
|
|
391
449
|
? process.env
|
|
392
450
|
: { ...process.env, LOGGER_LEVEL: "warning" }
|
|
@@ -399,7 +457,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
399
457
|
const waitOut = await new Response(waitProc.stdout).text()
|
|
400
458
|
const waitErr = await new Response(waitProc.stderr).text()
|
|
401
459
|
const waitExit = await waitProc.exited
|
|
402
|
-
const waitPayload =
|
|
460
|
+
const waitPayload = parseCliJson([waitOut, waitErr].filter(Boolean).join("\n"))
|
|
403
461
|
const waitMessage = typeof waitPayload?.message === "string" ? waitPayload.message.toLowerCase() : ""
|
|
404
462
|
const waitError = typeof waitPayload?.error === "string" ? waitPayload.error.toLowerCase() : ""
|
|
405
463
|
const waitTimedOut = waitMessage.includes("timeout") || waitError.includes("timed out")
|
|
@@ -416,7 +474,7 @@ export const ControlKeelGovernance = async ({ $, directory }) => {
|
|
|
416
474
|
manualApprovalRequired: true,
|
|
417
475
|
reason: "review_timeout",
|
|
418
476
|
guidance:
|
|
419
|
-
"Plan review is still pending after timeout. Show the `browser_url` to the user if reachable. If browser review is unavailable or the user explicitly approves in chat, record it with `controlkeel review plan respond
|
|
477
|
+
"Plan review is still pending after timeout. Show the `browser_url` to the user if reachable. If browser review is unavailable or the user explicitly approves in chat, record it with `controlkeel review plan respond <review_id> --decision approved --feedback-notes \"User approved in chat after timeout/browser issue\" --json` (or `ck_review_feedback`) before proceeding.",
|
|
420
478
|
})
|
|
421
479
|
}
|
|
422
480
|
|
package/package.json
CHANGED