Skip to content
Open
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,7 @@ pip-wheel-metadata/
.DS_Store
.idea/
.vscode/

#web
.next/
node_modules/
93 changes: 93 additions & 0 deletions Cargo.lock

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

7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ edition = "2024"

[dependencies]
anyhow = "1.0.100"
axum = "0.7.9"
candid = "0.10.20"
clap = { version = "4.5.51", features = ["derive"] }
hex = "0.4.3"
ic-agent = "0.44.3"
ic-agent = { version = "0.44.3", features = ["ring"] }
keyring = { version = "3", features = [
"apple-native",
"windows-native",
Expand All @@ -31,6 +32,10 @@ serde_json = "1.0.145"
pyo3 = { version = "0.27", features = ["extension-module", "abi3-py38"], optional = true }
pdf-extract = "0.8"
gag = "1.0"
ring = "0.17.14"
der = "0.7.10"
pkcs8 = "0.10.2"
ic-ed25519 = "0.2.0"

[features]
default = []
Expand Down
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,24 @@ dfx canister --ic call 73mez-iiaaa-aaaaq-aaasq-cai icrc1_balance_of '(record {ow

# Example: (100000000 : nat) == 1 KINIC
```

### 3. Internet Identity flow (--ii, CLI only)

If you prefer browser login instead of a keychain-backed dfx identity:

```bash
cargo run -- --ii login
cargo run -- --ii list
```

Delegations are stored at `~/.config/kinic/identity.json` (default TTL: 6 hours).
The login flow uses a local callback on port `8620`.

**DM https://x.com/wyatt_benno for KINIC prod tokens** with your principal ID.

Or purchase them from MEXC or swap at https://app.icpswap.com/ .

### 3. Deploy and Use Memory
### 4. Deploy and Use Memory

```python
from kinic_py import KinicMemories
Expand Down
23 changes: 22 additions & 1 deletion docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ Command-line companion for deploying and operating Kinic “memory” canisters.

## Running the CLI

All commands require `--identity`. Use `--ic` to talk to mainnet; omit it (or leave false) for the local replica.
Use either `--identity` (dfx identity name stored in the system keychain) or `--ii` (Internet Identity login). Use `--ic` to talk to mainnet; omit it (or leave false) for the local replica. If you are not using `--ii`, `--identity <name>` is required for CLI commands.

```bash
cargo run -- --identity alice list
Expand All @@ -57,6 +57,27 @@ cargo run -- --identity alice create \
--description "Local test canister"
```

### Internet Identity flow (--ii)

First, open the browser login flow and store a delegation (default TTL: 6 hours):

```bash
cargo run -- --ii login
```

Then run commands with `--ii`:

```bash
cargo run -- --ii list
cargo run -- --ii create \
--name "Demo memory" \
--description "Local test canister"
```

Notes:
- Delegations are stored at `~/.config/kinic/identity.json`.
- The login flow uses a local callback on port `8620`.

### Convert PDF to markdown (inspect only)

```bash
Expand Down
45 changes: 45 additions & 0 deletions docs/ii-login-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Internet Identity CLI Login Overview

Where
- Component: rust/commands/ii_login.rs
- Data store: ~/.config/kinic/identity.json (or --identity-path)

What
- The CLI opens a browser page that talks to Internet Identity.
- A local axum callback server receives delegations and stores them for future CLI calls.

Why
- Allows CLI-only login without relying on a keychain-backed dfx identity.

Flow (high level)
1) CLI generates a session key pair and a random state token, then starts a local HTTP listener on 127.0.0.1:8620.
- The session key pair is used to request a short-lived delegation from Internet Identity.
- The state token is embedded in the page and must match the callback payload.
- The local listener is the callback endpoint for the browser to POST the signed delegation.
- Binding to 127.0.0.1 ensures the callback is only reachable from the same machine.
2) CLI serves an HTML page that opens the Internet Identity authorize URL.
3) Internet Identity returns signed delegations to the local callback endpoint.
4) CLI verifies the delegation public key matches the session key.
5) CLI persists the delegation bundle with expiration and metadata to ~/.config/kinic/identity.json (or --identity-path).
- Stored fields include: identity provider URL, user public key, session key (pkcs8), delegations, expiration, created timestamp.
- Delegations may include target canisters; those targets are preserved in the saved delegation list.

Server lifetime
- The callback server accepts a single successful callback, then exits.
- If no valid callback arrives before the timeout, the login flow fails.

Key data exchanged
- Session public key (SPKI) from CLI to browser page.
- Delegations + user public key from browser to CLI callback.

Security notes
- The callback is bound to localhost only.
- Callback payloads are rejected if the state token does not match.
- Delegations are verified against the session key before saving.
- Expiration is computed and stored to prevent stale reuse.
- On reuse, the CLI validates the stored file, checks expiration, and normalizes/verifies the delegation chain.
- Callback requests must be JSON and are capped at 256 KB. If a Content-Length header is present, it is validated against the same limit.

Related files
- rust/commands/ii_login.rs
- rust/identity_store.rs
46 changes: 31 additions & 15 deletions rust/agent.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::io::Cursor;
use std::{io::Cursor, sync::Arc};

use anyhow::Result;
use ic_agent::{
Agent,
Agent, Identity,
export::reqwest::Url,
identity::{BasicIdentity, Secp256k1Identity},
};
Expand All @@ -14,31 +14,47 @@ pub const KEYRING_IDENTITY_PREFIX: &str = "internet_computer_identity_";
pub struct AgentFactory {
use_mainnet: bool,
identity_suffix: String,
identity_override: Option<Arc<dyn Identity>>,
}

impl AgentFactory {
pub fn new(use_mainnet: bool, identity_suffix: impl Into<String>) -> Self {
Self {
use_mainnet,
identity_suffix: identity_suffix.into(),
identity_override: None,
}
}

pub async fn build(&self) -> Result<Agent> {
let pem_bytes = load_pem_from_keyring(&self.identity_suffix)?;
let pem_text = String::from_utf8(pem_bytes.clone())?;
let pem = pem::parse(pem_text.as_bytes())?;
pub fn new_with_identity<I>(use_mainnet: bool, identity: I) -> Self
where
I: Identity + 'static,
{
Self {
use_mainnet,
identity_suffix: String::new(),
identity_override: Some(Arc::new(identity)),
}
}

let builder = match pem.tag() {
"PRIVATE KEY" => {
let identity = BasicIdentity::from_pem(Cursor::new(pem_text.clone()))?;
Agent::builder().with_identity(identity)
}
"EC PRIVATE KEY" => {
let identity = Secp256k1Identity::from_pem(Cursor::new(pem_text.clone()))?;
Agent::builder().with_identity(identity)
pub async fn build(&self) -> Result<Agent> {
let builder = if let Some(identity) = &self.identity_override {
Agent::builder().with_arc_identity(identity.clone())
} else {
let pem_bytes = load_pem_from_keyring(&self.identity_suffix)?;
let pem_text = String::from_utf8(pem_bytes.clone())?;
let pem = pem::parse(pem_text.as_bytes())?;
match pem.tag() {
"PRIVATE KEY" => {
let identity = BasicIdentity::from_pem(Cursor::new(pem_text.clone()))?;
Agent::builder().with_identity(identity)
}
"EC PRIVATE KEY" => {
let identity = Secp256k1Identity::from_pem(Cursor::new(pem_text.clone()))?;
Agent::builder().with_identity(identity)
}
_ => anyhow::bail!("Unsupported PEM tag: {}", pem.tag()),
}
_ => anyhow::bail!("Unsupported PEM tag: {}", pem.tag()),
};

let url = if self.use_mainnet {
Expand Down
Loading