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: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Quit OpenCode and Pi, then run:
npx @cortexkit/opencode-claustrum-auth setup
```

Setup detects the hosts and registers the appropriate package: `@cortexkit/opencode-claustrum-auth` for OpenCode or `@cortexkit/pi-claustrum-auth` for Pi. Approve enrollment and grant access in `ck auth`, then follow the printed provider activation commands. See [setup and activation](docs/setup.md) for a dry run, host selection and the separate grant step.
Setup detects the hosts and registers the appropriate package: `@cortexkit/opencode-claustrum-auth` for OpenCode or `@cortexkit/pi-claustrum-auth` for Pi. Approve enrollment in `ck auth` to grant model-key read access in the same step on vaults that support it, then follow the printed provider activation commands. Older vaults need the manual grant command. See [setup and activation](docs/setup.md) for a dry run, host selection and the fallback grant step.

## What it does

Expand Down
16 changes: 8 additions & 8 deletions bun.lock

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

17 changes: 13 additions & 4 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,19 @@ Vault mode is always on. `--claustrum` is accepted for compatibility;

Approve each host's separate enrollment in `ck auth`. The apps show
"Model API keys for OpenCode" or "Model API keys for Pi" when you approve the
connection. Setup prints the exact
approval and grant commands and the resolved state paths. Enrollment alone
does not grant credential access: follow the printed grant command for the
`llm-provider` category before activating a provider.
connection. Setup requests read access to the `llm-provider` category with
each enrollment, so approval grants that access in the same step on vaults
that support requested grants. Setup checks access after approval and prints
the resolved state paths and provider activation commands.

Older vaults may approve enrollment without applying the requested grant. If
access is missing or cannot be confirmed, follow setup's printed grant command
before activating a provider:

```sh
ck auth grant --principal enrolled:claustrum-auth-opencode --selector-kind category --selector llm-provider --operation read
ck auth grant --principal enrolled:claustrum-auth-pi --selector-kind category --selector llm-provider --operation read
```

Approve each additional instance of OpenCode or Pi with its own `--name`, and
use that same name in the grant's `--principal enrolled:<name>`.
Expand Down
4 changes: 2 additions & 2 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"test": "bun test"
},
"dependencies": {
"@cortexkit/claustrum-client": "^0.17.0",
"@cortexkit/common-auth": "^0.43.0",
"@cortexkit/claustrum-client": "^0.18.0",
"@cortexkit/common-auth": "^0.44.0",
"@cortexkit/private-files": "^0.2.2",
"@cortexkit/subc-client": "^0.24.0"
}
Expand Down
4 changes: 2 additions & 2 deletions packages/opencode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@
"test": "bun test"
},
"dependencies": {
"@cortexkit/claustrum-client": "^0.17.0",
"@cortexkit/common-auth": "^0.43.0",
"@cortexkit/claustrum-client": "^0.18.0",
"@cortexkit/common-auth": "^0.44.0",
"@cortexkit/private-files": "^0.2.2",
"@cortexkit/subc-client": "^0.24.0"
},
Expand Down
24 changes: 9 additions & 15 deletions packages/opencode/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import { printLine, processTerminal } from '@cortexkit/common-auth/auth-menu'
import {
connectClaustrumScopedClient,
hostEnrollmentPaths,
readClaustrumEnrollmentStatus,
VAULT_HOST_CONNECT_REFUSAL,
type VaultGrantReach,
vaultAuthority,
Expand Down Expand Up @@ -104,11 +103,17 @@ export async function runClaustrumSetup(
opencode: {
name: opencodeEnrollment,
displayName: 'Model API keys for OpenCode',
requestedGrants: [
{ selector: { category: 'llm-provider' }, operation: 'read' },
],
paths: { stateDir: states.opencode, host: 'opencode' },
},
pi: {
name: piEnrollment,
displayName: 'Model API keys for Pi',
requestedGrants: [
{ selector: { category: 'llm-provider' }, operation: 'read' },
],
paths: { stateDir: '', host: 'pi' },
},
},
Expand Down Expand Up @@ -194,20 +199,9 @@ export async function runClaustrumSetup(
await built.hub.dispose()
}
}
let grantName = enrollment.name
if (discover) {
const status = await readClaustrumEnrollmentStatus(
hostEnrollmentPaths(enrollment.paths),
enrollment.name,
)
if (status.state === 'approved' && status.approvedName)
grantName = status.approvedName
}
if (!reach || reach.grants === 0) {
if (discover && !reach)
print(
'Grant reach is unknown; if access has not already been granted, use the grant command below.',
)
// Enrollment prints confirmed access or the remedy. Dry runs skip enrollment.
if (!discover) {
const grantName = enrollment.name
if (/^[a-z0-9][a-z0-9-]*$/u.test(grantName))
print(
`Grant command: ck auth grant --principal enrolled:${grantName} --selector-kind category --selector llm-provider --operation read`,
Expand Down
147 changes: 145 additions & 2 deletions packages/opencode/test/setup.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { afterEach, expect, test } from 'bun:test'
import { spawnSync } from 'node:child_process'
import { existsSync } from 'node:fs'
import { chmod, mkdir, mkdtemp, readFile, rm, stat } from 'node:fs/promises'
import {
chmod,
mkdir,
mkdtemp,
readFile,
rm,
stat,
writeFile,
} from 'node:fs/promises'
import path from 'node:path'
import {
ClaustrumCredentialError,
enrollmentAuthority,
hostEnrollmentPaths,
} from '@cortexkit/common-auth/claustrum'
Expand Down Expand Up @@ -298,7 +307,7 @@ test('test setup build enrolls once per host, discovers rows, prints provider ac
)
})

test('setup proposes a person-facing display name for each vault host', async () => {
test('setup proposes a person-facing display name and provider read grant for each vault host', async () => {
const root = await profile()
const h = setupIO(root)
await runClaustrumSetup(
Expand All @@ -309,16 +318,150 @@ test('setup proposes a person-facing display name for each vault host', async ()
{
name: 'claustrum-auth-opencode',
displayName: 'Model API keys for OpenCode',
requestedGrants: [
{ selector: { category: 'llm-provider' }, operation: 'read' },
],
requestSecretHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
},
{
name: 'claustrum-auth-pi',
displayName: 'Model API keys for Pi',
requestedGrants: [
{ selector: { category: 'llm-provider' }, operation: 'read' },
],
requestSecretHash: expect.stringMatching(/^[a-f0-9]{64}$/u),
},
])
})

test.each(['reachable', 'no-logins', 'unconfirmed', 'older-vault'] as const)(
'setup relies on enrollment access guidance without duplicate grant hints (%s)',
async (access) => {
const root = await profile()
const h = setupIO(root)
const connect = h.io.connect
if (!connect) throw new Error('missing enrollment transport')
let lists = 0
h.io.connect = async (options) => {
const client = await connect(options)
return {
...client,
async enrollPropose(input) {
if (access === 'older-vault' && input.requestedGrants)
throw new ClaustrumCredentialError(
'invalid_params',
'permanent',
'gone',
)
return client.enrollPropose(input)
},
async listScoped() {
lists++
if (access === 'unconfirmed') throw new Error('vault unavailable')
return {
rows: access === 'reachable' ? [row('a')] : [],
view: 'setup',
grants: access === 'reachable' ? 1 : 0,
grantTuples: [],
}
},
}
}
await runClaustrumSetup(
['--yes', '--host=opencode', '--host=pi', '--opencode=both'],
h.io,
{
table: fixtureEntries,
createCore: (options) =>
createTestCore({
...options,
connect: async () => ({
async listScoped() {
return { rows: [], view: 'empty', grants: 0, grantTuples: [] }
},
async getScoped() {
throw new Error('setup must not read credential material')
},
async reportAuthFailureScoped() {},
close() {},
}),
}),
},
)
expect(lists).toBe(access === 'older-vault' ? 0 : 2)
const text = h.output()
if (access === 'reachable') {
expect(text).toContain('Confirmed read access')
expect(text).not.toContain('ck auth grant ')
} else {
expect(text).toContain(
access === 'older-vault'
? 'This vault is too old to grant access with the approval'
: access === 'no-logins'
? 'Approved, but no logins are reachable yet'
: 'Approved, but access could not be confirmed',
)
for (const host of ['opencode', 'pi'])
expect(text).toContain(
`ck auth grant --principal enrolled:claustrum-auth-${host} --selector-kind category --selector llm-provider --operation read`,
)
expect(text.match(/ck auth grant /gu)).toHaveLength(2)
}
expect(text).not.toContain('Grant command:')
expect(text).toContain('/claustrum use fixture-a')
expect(text).toContain(
'opencode auth login fixture-a --method claustrum-auth',
)
},
)

test.each([false, true])(
'setup confirms access on an already approved enrollment (without stored grants: %j)',
async (withoutStoredGrants) => {
const root = await profile()
const h = setupIO(root)
const args = ['--yes', '--enroll-only', '--host=opencode', '--host=pi']
await runClaustrumSetup(args, h.io)
if (withoutStoredGrants) {
const states = {
opencode: resolveOpencodeStateDir(h.env, root),
pi: path.join(h.env.PI_CODING_AGENT_DIR, 'claustrum-auth'),
}
for (const host of ['opencode', 'pi'] as const) {
const { statePath } = hostEnrollmentPaths({
stateDir: states[host],
host,
})
const state = JSON.parse(await readFile(statePath, 'utf8'))
delete state.requestedGrants
delete state.requestedGrantsStatus
await writeFile(statePath, JSON.stringify(state))
}
}
const connect = h.io.connect
if (!connect) throw new Error('missing enrollment transport')
let lists = 0
h.io.connect = async (options) => ({
...(await connect(options)),
async listScoped() {
lists++
return { rows: [row('a')], view: 'setup', grants: 1, grantTuples: [] }
},
})
const start = h.output().length
await runClaustrumSetup(args, h.io)
expect(lists).toBe(2)
expect(h.proposals).toEqual([
'claustrum-auth-opencode',
'claustrum-auth-pi',
])
const text = h.output().slice(start)
expect(text.match(/Already connected as /gu)).toHaveLength(2)
expect(text.match(/Confirmed read access /gu)).toHaveLength(2)
expect(text).not.toContain('ck auth grant ')
},
)

test('setup refuses local sign-in and delegates enroll-only without host registration or credential reads', async () => {
const root = await profile()
const h = setupIO(root)
Expand Down
4 changes: 2 additions & 2 deletions packages/pi/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
"test": "bun test"
},
"dependencies": {
"@cortexkit/claustrum-client": "^0.17.0",
"@cortexkit/common-auth": "^0.43.0",
"@cortexkit/claustrum-client": "^0.18.0",
"@cortexkit/common-auth": "^0.44.0",
"@cortexkit/private-files": "^0.2.2",
"@cortexkit/subc-client": "^0.24.0"
},
Expand Down
Loading