kira-wallet 0.1.15 → 0.1.16
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/THIRD_PARTY_NOTICES.md +18 -0
- package/cashout-quotes.cjs +12 -3
- package/cashout.py +1 -0
- package/chain_lifecycle.py +5 -5
- package/docs/AGENT_TOOLS.md +22 -1
- package/docs/JOB_CONTRACT.md +12 -4
- package/docs/RELEASE.md +60 -3
- package/docs/WALLET_CAPABILITIES.md +65 -0
- package/holdings_query.py +74 -0
- package/kira_agent.py +33 -11
- package/kira_jobs.py +20 -1
- package/kira_research_tools.py +10 -1
- package/market-discovery.cjs +48 -0
- package/market-links.cjs +9 -0
- package/market_discovery.py +120 -0
- package/market_records.py +101 -0
- package/package.json +10 -3
- package/pipeline-onchain.cjs +6 -5
- package/sell-quotes.cjs +51 -0
- package/sell_quotes.py +157 -0
- package/viewer/README.md +1 -1
- package/viewer/details.py +12 -5
- package/viewer/model.py +60 -12
- package/viewer/refresh-curve-prices.cjs +1 -1
- package/viewer/refresh_prices.py +19 -5
- package/viewer/server.py +1 -1
- package/viewer/static/app.js +68 -31
- package/viewer/static/chat-workspace.css +121 -17
- package/viewer/static/chat-workspace.js +1 -1
- package/viewer/static/explorers/etherscan.png +0 -0
- package/viewer/static/index.html +23 -7
- package/viewer/static/jobs.js +51 -20
- package/viewer/static/markdown.js +12 -1
- package/viewer/static/selects.js +9 -5
- package/viewer/static/style.css +3 -8
- package/viewer/static/venues/aerodrome.svg +32 -0
- package/viewer/static/venues/mintclub.png +0 -0
- package/viewer/static/venues/uniswap.svg +6 -0
- package/viewer/static/workspace-model.js +53 -26
- package/viewer/static/workspace.css +87 -0
- package/viewer/static/workspace.js +10 -5
- package/viewer/token_images.py +5 -1
- package/wallet.py +27 -23
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -47,3 +47,21 @@ per-contract provenance is in the catalog.
|
|
|
47
47
|
## Alchemy setup illustration
|
|
48
48
|
|
|
49
49
|
`viewer/static/alchemy-create-app.png` is an unmodified example screenshot from Alchemy’s public API key walkthrough, used to explain app setup. Source: https://www.alchemy.com/docs/create-an-api-key . The image contains no Kira operator account or credential. Alchemy retains rights to its interface and illustration.
|
|
50
|
+
|
|
51
|
+
## Market platform marks
|
|
52
|
+
|
|
53
|
+
The local market cards identify their sources using the official marks retrieved
|
|
54
|
+
on October 19, 2026: [Mint Club app icon](https://mint.club/apple-touch-icon.png),
|
|
55
|
+
[Uniswap logo](https://github.com/Uniswap/interface/blob/main/packages/ui/src/assets/icons/uniswap-logo.svg),
|
|
56
|
+
and [Aerodrome favicon](https://aerodrome.finance/svg/AERO/favicon.svg).
|
|
57
|
+
These marks identify external services and remain their respective owners' marks.
|
|
58
|
+
Kira does not claim affiliation or endorsement. The original files are served
|
|
59
|
+
locally from `viewer/static/venues/`.
|
|
60
|
+
|
|
61
|
+
## Explorer mark
|
|
62
|
+
|
|
63
|
+
The Etherscan icon in `viewer/static/explorers/` is used for explorer links.
|
|
64
|
+
Each link retains its destination explorer name or the generic Explorer label.
|
|
65
|
+
It was retrieved unchanged on October 19, 2026 from
|
|
66
|
+
[Etherscan documentation](https://docs.etherscan.io/mintlify-assets/_mintlify/favicons/etherscan/PMbbZQeU1A_1WqNP/_generated/favicon/android-chrome-192x192.png).
|
|
67
|
+
The mark belongs to its owner and does not imply affiliation or endorsement.
|
package/cashout-quotes.cjs
CHANGED
|
@@ -25,7 +25,7 @@ function contractRevert(error) {
|
|
|
25
25
|
}
|
|
26
26
|
return reverted;
|
|
27
27
|
}
|
|
28
|
-
async function quotePool(client,network,token,pool,block,blockHash,deployments,{batchRead}={}) {
|
|
28
|
+
async function quotePool(client,network,token,pool,block,blockHash,deployments,{batchRead,fullOnly=false}={}) {
|
|
29
29
|
const state=pool.factory_measurement?.state||{},balance=integer(token.wallet_balance_raw);
|
|
30
30
|
const unknown=reason=>({status:'unknown',reason,gas_included:false,execution_verified:false});
|
|
31
31
|
if(!balance||!address(token.token_address)||!address(state.token0)||!address(state.token1)||state.token0.toLowerCase()===state.token1.toLowerCase()
|
|
@@ -43,7 +43,16 @@ async function quotePool(client,network,token,pool,block,blockHash,deployments,{
|
|
|
43
43
|
if(canonical('v3')&"er&&Number.isInteger(state.fee)&&state.fee>=0&&state.fee<1_000_000) {
|
|
44
44
|
kind='uniswap_v3_quoter';
|
|
45
45
|
request=amount=>({address:quoter.address,abi:v3Abi,functionName:'quoteExactInputSingle',args:[{tokenIn:token.token_address,tokenOut:output,amountIn:amount,fee:state.fee,sqrtPriceLimitX96:0n}]});
|
|
46
|
-
decode=result=>
|
|
46
|
+
decode=result=>{
|
|
47
|
+
// QuoterV2's default limit is MIN_SQRT_RATIO+1 / MAX_SQRT_RATIO-1
|
|
48
|
+
// (Uniswap v3-core TickMath). At that boundary input can be unspent.
|
|
49
|
+
if(fullOnly){
|
|
50
|
+
const after=integer(result[1]);
|
|
51
|
+
if(after===null||after<=4295128740n||after>=1461446703485210103287273052203988822378723970341n)
|
|
52
|
+
throw Error('Full input consumption is not established at the pool price limit');
|
|
53
|
+
}
|
|
54
|
+
return result[0];
|
|
55
|
+
};
|
|
47
56
|
}else if(network.chain_id===8453&&state.factory?.toLowerCase()==='0x420dd381b31aef6683db6b902084cb0ffece40da'&&pool.venue==='aerodrome') {
|
|
48
57
|
kind='aerodrome_pool';
|
|
49
58
|
request=amount=>({address:pool.pool,abi:aeroAbi,functionName:'getAmountOut',args:[amount,token.token_address]});
|
|
@@ -54,7 +63,7 @@ async function quotePool(client,network,token,pool,block,blockHash,deployments,{
|
|
|
54
63
|
kind='uniswap_v2_reserves';quote=async amount=>v2AmountOut(amount,reserveIn,reserveOut);
|
|
55
64
|
}else return unknown('Quote adapter unavailable for this verified pool');
|
|
56
65
|
if(request)quote=async amount=>decode(await client.readContract({...request(amount),blockNumber:block}));
|
|
57
|
-
const amounts=inputAmounts(balance);let failures=0,buffer=[];
|
|
66
|
+
const amounts=fullOnly?[balance]:inputAmounts(balance);let failures=0,buffer=[];
|
|
58
67
|
for(let index=0;index<amounts.length;index++) {
|
|
59
68
|
const amount=amounts[index];
|
|
60
69
|
let result;
|
package/cashout.py
CHANGED
|
@@ -136,6 +136,7 @@ def classify(token, market, *, environment='mainnet'):
|
|
|
136
136
|
proceeds = output * Decimal(price['usd'])
|
|
137
137
|
result['quotes'].append({'kind': quote.get('kind'), 'input_amount_raw': str(quote['input_amount_raw']),
|
|
138
138
|
'output_amount': format(output, 'f'), 'output_address': quote['output_address'].lower(),
|
|
139
|
+
'output_symbol': quote.get('output_symbol'),
|
|
139
140
|
'quoted_output_usd': format(proceeds, 'f'), 'qualifies': proceeds >= Decimal('1'),
|
|
140
141
|
'block_number': block, 'block_hash': block_hash, 'observed_at': quote['observed_at'],
|
|
141
142
|
'output_price': price, 'partial_input': amount < holding,
|
package/chain_lifecycle.py
CHANGED
|
@@ -22,9 +22,9 @@ NOTICES={
|
|
|
22
22
|
'notice_url':'https://www.alchemy.com/docs/reference/degen-deprecation-notice',
|
|
23
23
|
'network_shutdown_date':None,'provider_support_ended_on':'2026-08-31',
|
|
24
24
|
'note':'Alchemy describes Degen L3 as shut down. Its provider support ended August 31. The exact network shutdown date is not independently established. Base DEGEN is a separate token.'},
|
|
25
|
-
|
|
26
|
-
'notice_url':None,'
|
|
27
|
-
|
|
25
|
+
**{cid:{'status':'retired','verified':False,'confirmation':'operator','reviewed_on':'2026-10-19',
|
|
26
|
+
'notice_url':None,'note':'Excluded from new research by operator confirmation. Historical observations are preserved; no independent closure notice is claimed.'}
|
|
27
|
+
for cid in (5112,54176)},
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
|
|
@@ -32,7 +32,7 @@ def lifecycle(chain_id):
|
|
|
32
32
|
state=dict(NOTICES.get(chain_id,{'status':'unconfirmed','verified':False,
|
|
33
33
|
'notice_url':None,'note':'No verified retirement notice is recorded. Connectivity and discovery are separate observations.'}))
|
|
34
34
|
state.update(reference_url=REFERENCES.get(chain_id,state.get('reference_url')),
|
|
35
|
-
reviewed_on=REVIEWED_ON if chain_id in REFERENCES or chain_id in NOTICES else None,
|
|
35
|
+
reviewed_on=state.get('reviewed_on') or REVIEWED_ON if chain_id in REFERENCES or chain_id in NOTICES else None,
|
|
36
36
|
audit_result='reference_unavailable' if chain_id in UNAVAILABLE_REFERENCES else 'reference_retrieved' if chain_id in REFERENCES else 'notice_reviewed' if chain_id in NOTICES else 'not_reviewed')
|
|
37
37
|
if chain_id==8217:
|
|
38
38
|
state['provider_notice']={'provider':'The Graph','support_ended_on':'2026-08-31','url':'https://docs.kaia.io/build/tools/indexers/','note':'Indexer support ended. This does not establish network retirement.'}
|
|
@@ -41,4 +41,4 @@ def lifecycle(chain_id):
|
|
|
41
41
|
|
|
42
42
|
def retired(chain_id):
|
|
43
43
|
state=lifecycle(chain_id)
|
|
44
|
-
return state['status']=='retired' and state['verified'] is True
|
|
44
|
+
return state['status']=='retired' and (state['verified'] is True or state.get('confirmation')=='operator')
|
package/docs/AGENT_TOOLS.md
CHANGED
|
@@ -32,7 +32,7 @@ wallet scope and research permission. It sends no wallet facts. Authentication
|
|
|
32
32
|
stays with the installed CLI. No desktop tokens or authentication files are copied.
|
|
33
33
|
|
|
34
34
|
With research enabled, a bounded host-mediated loop provides portfolio_read,
|
|
35
|
-
wallet_read, token_read, snapshots_list, snapshot_read, snapshots_compare,
|
|
35
|
+
wallet_read, token_read, holdings_query, snapshots_list, snapshot_read, snapshots_compare,
|
|
36
36
|
job_list, job_read, holdings_refresh and prices_refresh. Every operation is
|
|
37
37
|
scoped to registered wallets allowed by the conversation. Historical reads use
|
|
38
38
|
normalized financial projections. Unknown values and historical quote times
|
|
@@ -104,3 +104,24 @@ rpc_status pending means a network has not yet been researched. It does not
|
|
|
104
104
|
prove an RPC outage. For a curve comparison, match chain and contract and
|
|
105
105
|
compare reserve, supply, next-mint price and royalty observations. A newer
|
|
106
106
|
block or a different wallet-sized burn quote alone cannot prove curve movement.
|
|
107
|
+
|
|
108
|
+
## Bounded full-inventory queries
|
|
109
|
+
|
|
110
|
+
Connected chat uses `holdings_query` for names, rankings and aggregates. Its
|
|
111
|
+
`mode` is `search`, `rank` or `aggregate`. It scans every approved saved position
|
|
112
|
+
on the host and returns at most 25 results per page. This response bound never
|
|
113
|
+
limits holdings discovery. Optional filters include wallet, chain, environment
|
|
114
|
+
and minimum recorded USD value. Exact symbols, names and contracts take
|
|
115
|
+
precedence over substring matches; multiple chain/contract identities remain
|
|
116
|
+
explicitly ambiguous. `group_by: asset` combines the priced subset by identity,
|
|
117
|
+
while `position` keeps each wallet position separate. Unknown and excluded
|
|
118
|
+
positions have independent counts. Ranking cannot create a price for them.
|
|
119
|
+
|
|
120
|
+
Top-N completion requires a delivered full-inventory ranking receipt, including
|
|
121
|
+
natural requests such as “top five.” Search pages must be delivered through the
|
|
122
|
+
last match before claiming that all matches were read. A host count or sum does
|
|
123
|
+
not require sending every position to the model. These reads never queue jobs.
|
|
124
|
+
The existing 32-read plan, eight-round, prompt/context and time limits remain.
|
|
125
|
+
Detailed internal receipts are kept locally; partial-result messages use plain
|
|
126
|
+
language. Safe absolute HTTP(S) Markdown links are clickable; raw HTML remains
|
|
127
|
+
escaped and images, credential URLs and active URL schemes stay disabled.
|
package/docs/JOB_CONTRACT.md
CHANGED
|
@@ -44,10 +44,18 @@ no absolute paths or arbitrary file reads are supported.
|
|
|
44
44
|
|
|
45
45
|
The optional `started_at` timestamp records when the current attempt was
|
|
46
46
|
claimed by the worker. Resuming replaces it on the next claim. Older envelopes
|
|
47
|
-
remain valid without it. The viewer then labels its timer "Since request"
|
|
48
|
-
instead of claiming an exact execution
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
remain valid without it. The viewer then labels its active timer "Since request"
|
|
48
|
+
and its terminal timer "Request duration" instead of claiming an exact execution
|
|
49
|
+
time. Terminal attempt and total timers stop at the last recorded event, falling
|
|
50
|
+
back to the saved update time. Total duration includes waiting and pauses across
|
|
51
|
+
attempts. A queued job uses its latest queue transition, not a previous attempt's
|
|
52
|
+
start.
|
|
53
|
+
|
|
54
|
+
Completed sell analysis leaves the wallet's progress card. An observed transition
|
|
55
|
+
from queued or running to succeeded or partial shows a brief completion notice.
|
|
56
|
+
Loading saved history does not repeat that notice. Activity retains the result,
|
|
57
|
+
unknown checks, stages and stopped timers. Failed, paused and interrupted work
|
|
58
|
+
keeps its recovery controls.
|
|
51
59
|
|
|
52
60
|
States: queued -> running -> succeeded | partial | failed | cancelled.
|
|
53
61
|
Recovery converts unfinished running jobs to interrupted. Explicit resume may
|
package/docs/RELEASE.md
CHANGED
|
@@ -79,7 +79,7 @@ The earlier unpublished 0.1.2 candidate is preserved in the ignored
|
|
|
79
79
|
`dist/preserved-0.1.2-bad8f02/` directory. Use the current `dist/release.json`
|
|
80
80
|
and its matching archive for publication; do not mix manifests from candidates.
|
|
81
81
|
|
|
82
|
-
Keep published GitHub tags, release assets and npm versions through 0.1.
|
|
82
|
+
Keep published GitHub tags, release assets and npm versions through 0.1.15
|
|
83
83
|
immutable. Do not rebuild them from later documentation commits. Future package
|
|
84
84
|
changes need a new version. Release preparation does not publish a GitHub
|
|
85
85
|
release, promote the website or publish to npm.
|
|
@@ -95,12 +95,12 @@ python3 scripts/prepare-release.py
|
|
|
95
95
|
```
|
|
96
96
|
|
|
97
97
|
`prepare-release.py` scans the package boundary and creates
|
|
98
|
-
`dist/kira-wallet-0.1.
|
|
98
|
+
`dist/kira-wallet-0.1.16.tgz`. The ignored `dist/release.json` records its source
|
|
99
99
|
commit, SHA-256 and member count. It never publishes. After reviewing that
|
|
100
100
|
manifest, the operator's final command from the project root is:
|
|
101
101
|
|
|
102
102
|
```sh
|
|
103
|
-
npm publish ./dist/kira-wallet-0.1.
|
|
103
|
+
npm publish ./dist/kira-wallet-0.1.16.tgz --access public
|
|
104
104
|
```
|
|
105
105
|
|
|
106
106
|
Use the operator's npm account and complete authentication or OTP personally.
|
|
@@ -432,3 +432,60 @@ dry-run that exact archive. npm publication remains operator-only. Preserve the
|
|
|
432
432
|
personal 0.1.14 installation, credentials, paused history and published archives.
|
|
433
433
|
Use a separate private workspace for candidate acceptance. No GitHub release,
|
|
434
434
|
website deployment or personal app upgrade is part of this preparation.
|
|
435
|
+
|
|
436
|
+
## Release candidate: 0.1.16
|
|
437
|
+
|
|
438
|
+
This version repairs Mint Club symbol routes, including ERC1155 NFT routes and
|
|
439
|
+
encoded symbols, without rewriting historical snapshots. Market lookup attaches
|
|
440
|
+
both pair members and price updates persist new pools. Pool identity preserves
|
|
441
|
+
Uniswap v4 IDs, versions and fee tiers. Visible market records are independent
|
|
442
|
+
from price eligibility and executable proceeds. Existing thin-market, testnet
|
|
443
|
+
and unknown-value protections remain.
|
|
444
|
+
|
|
445
|
+
The basic market pass supplements Mint Club ERC20 holdings with official
|
|
446
|
+
Uniswap v2/v3/v4 index records and bounded Base common-quote factory reads.
|
|
447
|
+
Provider errors and incomplete pages remain partial. This is not proof of an
|
|
448
|
+
exhaustive blockchain-wide pool inventory. It does not require sell analysis.
|
|
449
|
+
Prior successful market observations survive missing sources; metadata-only
|
|
450
|
+
factory reads cannot refresh the date of an old price or liquidity observation.
|
|
451
|
+
|
|
452
|
+
Connected AI can search symbols and names, rank the full saved inventory, and
|
|
453
|
+
compute exact filtered aggregates without reading every inventory page into its
|
|
454
|
+
context. Official market marks and token/reserve artwork identify pool cards.
|
|
455
|
+
Ham and OverProtocol are excluded by operator confirmation, distinguished from
|
|
456
|
+
independently verified notices. Historical evidence is preserved. Successful
|
|
457
|
+
price metadata batches are reused for artwork to avoid redundant source reads.
|
|
458
|
+
|
|
459
|
+
Holdings give markets a separate row, retain every saved pool and open known
|
|
460
|
+
venue destinations directly. Token-title Explorer links use the locally served
|
|
461
|
+
Etherscan mark. Chain selectors show network artwork, sidebar actions align,
|
|
462
|
+
and All tokens adds the same value filters as wallet holdings.
|
|
463
|
+
|
|
464
|
+
Wallet, All tokens and network lists compare known value with saved sell-out
|
|
465
|
+
estimates. Basic holdings and explicit price refreshes collect bounded direct
|
|
466
|
+
pool quotes without viewer-triggered RPC reads or additional route discovery.
|
|
467
|
+
Uniswap v2/v3 and Base Aerodrome classic pools are supported. Full-balance
|
|
468
|
+
quantities are checked at the quote block. Unknown, unsupported, changed-input,
|
|
469
|
+
testnet and archived positions cannot become a USD sale estimate. Output-price
|
|
470
|
+
and quote clocks remain distinct. Independent quotes sharing liquidity are not
|
|
471
|
+
summed into portfolio proceeds; gas and execution guarantees remain excluded.
|
|
472
|
+
|
|
473
|
+
The wallet header separates identity, holdings status, valuation and optional
|
|
474
|
+
sell analysis. New wallets can show progress before their first snapshot. A
|
|
475
|
+
retired-network catalog mismatch no longer stops new holdings jobs. Failed work
|
|
476
|
+
offers typed recovery guidance and preserves prior results and failure history.
|
|
477
|
+
Completed sell analysis dismisses its progress card and briefly announces saved
|
|
478
|
+
results. Activity retains stages, unresolved checks and durations frozen at the
|
|
479
|
+
terminal event. Paused, interrupted and failed work retains recovery actions.
|
|
480
|
+
|
|
481
|
+
Imported wallets and autonomous execution are deferred to post-MVP. The decision
|
|
482
|
+
and reconsideration conditions are recorded in [wallet capabilities](WALLET_CAPABILITIES.md).
|
|
483
|
+
This release adds no transaction signing, token approval or trading capability.
|
|
484
|
+
|
|
485
|
+
Release acceptance covers the repository suite, clean installation, source and
|
|
486
|
+
history privacy checks, exact archive membership, real unchanged natural-language
|
|
487
|
+
questions and responsive market interactions. Review not selected. Publication
|
|
488
|
+
remains operator-only. A feature-branch candidate is not a merged release or a
|
|
489
|
+
published package; the final manifest must identify the accepted source commit.
|
|
490
|
+
Preserve the superseded unpublished archive before creating the final 0.1.16
|
|
491
|
+
candidate. Only the archive matching the final manifest is a publication target.
|
|
@@ -1,5 +1,70 @@
|
|
|
1
1
|
# Wallet capability direction
|
|
2
2
|
|
|
3
|
+
## Post-MVP decision: Imported wallets and execution, 2026-10-19
|
|
4
|
+
|
|
5
|
+
The operator accepted deferring the proposed Imported wallet execution stack
|
|
6
|
+
after the architecture, dependency and security discussion. It is post-MVP
|
|
7
|
+
research, not an implementation ticket or permission to request wallet grants.
|
|
8
|
+
This decision supersedes any earlier suggestion to begin a three-group rollout.
|
|
9
|
+
|
|
10
|
+
Keep the current Watching and OWS experiences for the MVP. External browser
|
|
11
|
+
connections remain public-address sources for Watching wallets. Existing
|
|
12
|
+
human-submitted encrypted OWS creation and public-descriptor connection are
|
|
13
|
+
implemented; transaction signing, transfers and trading are not implemented.
|
|
14
|
+
Do not add a third wallet group before its distinct capability is approved.
|
|
15
|
+
|
|
16
|
+
The evaluated future groups are Watching wallet, OWS wallet and Imported wallet.
|
|
17
|
+
For this proposal, Imported means connecting an existing external account and
|
|
18
|
+
explicit execution permissions while retaining its original keys in that wallet.
|
|
19
|
+
It does not mean importing a seed phrase or private key into Kira. Keep account
|
|
20
|
+
origin separate from execution capability. A future conversion must preserve the
|
|
21
|
+
same account's holdings and history without double-counting the portfolio.
|
|
22
|
+
|
|
23
|
+
The recommendation is to defer autonomous execution. Dependency size can be
|
|
24
|
+
bounded, but grant enforcement, signer isolation, transaction validation, nonce
|
|
25
|
+
ownership, duplicate prevention, revocation and uncertain-result recovery create
|
|
26
|
+
substantial security and maintenance obligations. Saved sell-out estimates are
|
|
27
|
+
research evidence and cannot be treated as ready-to-submit transactions.
|
|
28
|
+
|
|
29
|
+
A metadata-only dependency experiment using Smart Accounts Kit 2.0.0 increased
|
|
30
|
+
the lockfile package entries, excluding the root, from 18 to 53. Existing package
|
|
31
|
+
versions did not change. The SDK's own reported unpacked size was 3,156,894 bytes,
|
|
32
|
+
excluding its dependencies. This is not a browser bundle, memory or runtime
|
|
33
|
+
benchmark. No SDK was installed in the project and no signing code was run.
|
|
34
|
+
|
|
35
|
+
Reconsider only after a concrete recurring action justifies execution support:
|
|
36
|
+
|
|
37
|
+
- Begin with one chain and one supported action, confirmed per transaction in
|
|
38
|
+
the external wallet. Native key import remains outside this proposal.
|
|
39
|
+
- Consider limited automation later as an optional execution component. Define
|
|
40
|
+
actual on-chain restrictions for amounts, recipients, contracts and expiry;
|
|
41
|
+
local application checks alone do not constrain a compromised signing key.
|
|
42
|
+
- Keep private signing material outside model processes, prompts, browser
|
|
43
|
+
storage, research records and logs. Package or process separation alone is
|
|
44
|
+
not proof of an effective security boundary.
|
|
45
|
+
- Verify the granted permissions rather than assuming they equal the request.
|
|
46
|
+
Treat pause, disconnect and confirmed permission revocation as separate states.
|
|
47
|
+
- Validate current balances, exact calldata, fees and minimum output before
|
|
48
|
+
submission. Recover ambiguous sends without automatic duplicate transactions.
|
|
49
|
+
- Verify expiry, revocation, rejected actions and recovery with synthetic and
|
|
50
|
+
testnet evidence before considering mainnet. Independent security review is
|
|
51
|
+
recommended for that future scope and remains operator-selected.
|
|
52
|
+
|
|
53
|
+
MetaMask Advanced Permissions was a researched candidate, not an operator-selected
|
|
54
|
+
integration. ERC-7715 remains a draft and supported permission types and networks
|
|
55
|
+
must be checked again when this work is scheduled. OWS execution would need the
|
|
56
|
+
same security assessment; its current in-process policy and key handling are not
|
|
57
|
+
an on-chain or hardware isolation guarantee. Review not selected.
|
|
58
|
+
|
|
59
|
+
Primary references checked on 2026-10-19:
|
|
60
|
+
[ERC-7715](https://eips.ethereum.org/EIPS/eip-7715),
|
|
61
|
+
[EIP-7702 security](https://eips.ethereum.org/EIPS/eip-7702#security-considerations),
|
|
62
|
+
[MetaMask supported permissions](https://docs.metamask.io/smart-accounts-kit/get-started/supported-advanced-permissions/),
|
|
63
|
+
[delegation revocation](https://docs.metamask.io/smart-accounts-kit/guides/delegation/disable-delegation/),
|
|
64
|
+
[OWS key isolation](https://github.com/open-wallet-standard/core/blob/main/docs/05-key-isolation.md).
|
|
65
|
+
|
|
66
|
+
## Historical design context: 2026-10-04
|
|
67
|
+
|
|
3
68
|
Agreed with the operator on 2026-10-04: begin with Watching wallet connection.
|
|
4
69
|
Transfers, swaps and OWS Managed wallets need separate design and verification
|
|
5
70
|
before implementation. This is a future direction, not a delivered capability.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Bounded results from the complete authorized saved projection, without model paging."""
|
|
2
|
+
from decimal import Decimal, localcontext
|
|
3
|
+
import json
|
|
4
|
+
from kira_jobs import JobError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def query_holdings(wallets, args):
|
|
8
|
+
mode=args['mode'];query=args.get('query','').strip().casefold()
|
|
9
|
+
if mode not in ('search','rank','aggregate'):raise JobError('invalid_tool','Choose search, rank or aggregate.')
|
|
10
|
+
if mode=='search' and not query:raise JobError('invalid_tool','Supply a token symbol, name, contract or native.')
|
|
11
|
+
group_by=args.get('group_by','asset')
|
|
12
|
+
if group_by not in ('asset','position'):raise JobError('invalid_tool','Group by asset or position.')
|
|
13
|
+
environment=args.get('environment','mainnet')
|
|
14
|
+
if environment not in ('mainnet','testnet','all'):raise JobError('invalid_tool','Choose mainnet, testnet or all.')
|
|
15
|
+
limit=args.get('limit',10);offset=args.get('offset',0)
|
|
16
|
+
if limit>25:raise JobError('invalid_tool','Use at most 25 query results; this does not limit the scanned inventory.')
|
|
17
|
+
minimum=Decimal(str(args['minimum_value_usd'])) if 'minimum_value_usd' in args else None
|
|
18
|
+
def eligible(asset):
|
|
19
|
+
return not asset.get('archived') and (environment=='all' or asset.get('environment')==environment) and ('chain_id' not in args or asset['chain_id']==args['chain_id'])
|
|
20
|
+
def exact(asset):
|
|
21
|
+
return query in {str(asset.get('symbol','')).casefold(),str(asset.get('name','')).casefold(),str(asset.get('address','')).casefold()} or query=='native' and asset.get('is_native')
|
|
22
|
+
exact_match=bool(query) and any(eligible(a) and exact(a) for w in wallets for a in w['assets'])
|
|
23
|
+
scanned=0;selected=[];outside=0;unpriced=0
|
|
24
|
+
for wallet in wallets:
|
|
25
|
+
for asset in wallet['assets']:
|
|
26
|
+
scanned+=1
|
|
27
|
+
if 'chain_id' in args and asset['chain_id']!=args['chain_id']:continue
|
|
28
|
+
if query and not (exact(asset) if exact_match else query in str(asset.get('name','')).casefold() or query in str(asset.get('symbol','')).casefold()):continue
|
|
29
|
+
if asset.get('archived') or environment!='all' and asset.get('environment')!=environment:
|
|
30
|
+
outside+=1;continue
|
|
31
|
+
value=asset.get('value_usd') if asset.get('environment')=='mainnet' else None
|
|
32
|
+
if value is None:unpriced+=1
|
|
33
|
+
if minimum is not None and (value is None or Decimal(str(value))<minimum):continue
|
|
34
|
+
selected.append((wallet,asset))
|
|
35
|
+
from kira_agent import recorded_valuation, wallet_facts
|
|
36
|
+
valuation=recorded_valuation([a for _,a in selected])
|
|
37
|
+
groups={}
|
|
38
|
+
for wallet,asset in selected:
|
|
39
|
+
key=asset['id'] if group_by=='asset' else wallet['address'].lower()+':'+asset['id']
|
|
40
|
+
groups.setdefault(key,[]).append((wallet,asset))
|
|
41
|
+
ranked=[]
|
|
42
|
+
for key,positions in groups.items():
|
|
43
|
+
values=[Decimal(str(a['value_usd'])) for _,a in positions if a.get('value_usd') is not None and a.get('environment')=='mainnet']
|
|
44
|
+
with localcontext() as context:
|
|
45
|
+
context.prec=100;total=sum(values,Decimal(0)) if values else None
|
|
46
|
+
ranked.append((key,positions,total))
|
|
47
|
+
if mode=='rank':ranked=[r for r in ranked if r[2] is not None]
|
|
48
|
+
ranked.sort(key=lambda r:(r[2] is None,-r[2] if r[2] is not None else Decimal(0),r[0]))
|
|
49
|
+
page=ranked[offset:offset+limit] if mode!='aggregate' else []
|
|
50
|
+
rows=[];remaining=40000
|
|
51
|
+
for key,positions,total in page:
|
|
52
|
+
facts=[]
|
|
53
|
+
for wallet,asset in positions:
|
|
54
|
+
fact=wallet_facts([{**wallet,'assets':[asset]}],pool_budget=max(0,remaining))[0]['assets'][0]
|
|
55
|
+
remaining-=len(json.dumps(fact.get('pools',[])).encode())
|
|
56
|
+
facts.append({'wallet':wallet['address'],'wallet_name':wallet['name'],'snapshot_id':wallet.get('snapshot_id'),'asset':fact})
|
|
57
|
+
rows.append({'identity':positions[0][1]['id'],'value_usd':format(total,'f') if total is not None else None,
|
|
58
|
+
'position_count':len(positions),'unpriced_position_count':len(positions)-sum(a.get('value_usd') is not None and a.get('environment')=='mainnet' for _,a in positions),
|
|
59
|
+
'positions':facts})
|
|
60
|
+
count=len(ranked)
|
|
61
|
+
response={'mode':mode,'match_type':'exact' if exact_match else 'substring' if query else 'all','group_by':group_by,'environment':environment,'scanned_position_count':scanned,
|
|
62
|
+
'matched_position_count':len(selected),'matched_asset_count':len({a['id'] for _,a in selected}),
|
|
63
|
+
'unpriced_position_count':unpriced,'outside_scope_count':outside,'valuation':valuation,
|
|
64
|
+
'result_count':count,'results':rows,'offset':offset,'next_offset':offset+len(rows) if mode!='aggregate' and offset+len(rows)<count else None,
|
|
65
|
+
'ambiguous':mode=='search' and len({a['id'] for _,a in selected})>1,
|
|
66
|
+
'query_scope':{'mode':mode,'full_inventory_scanned':True,'requested_limit':limit,'offset':offset,'result_count':count},
|
|
67
|
+
'note':'Exact query over approved saved holdings, including records beyond the first page. Ranking sums the priced mainnet subset per chain and contract; unknown values remain separate. Not live wealth or sale proceeds. Search may match multiple identities; report the ambiguity rather than guessing. Missing records are unknown.'}
|
|
68
|
+
# Never enlarge the native context to accommodate a pathological result.
|
|
69
|
+
while len(json.dumps(response,ensure_ascii=False).encode())>120000 and rows:
|
|
70
|
+
rows.pop();response['next_offset']=offset+len(rows)
|
|
71
|
+
if mode!='aggregate' and page and not rows:raise JobError('context_limit','This match exceeds the detail limit. Use a wallet filter or token_read.')
|
|
72
|
+
response['query_scope']['returned_count']=len(rows)
|
|
73
|
+
response['query_scope']['complete']=mode=='aggregate' or len(rows)==len(page)
|
|
74
|
+
return response
|
package/kira_agent.py
CHANGED
|
@@ -30,6 +30,7 @@ SYSTEM = ('You are Kira, a careful wallet research partner. Answer in the langua
|
|
|
30
30
|
'The current recorded context supersedes older facts in the conversation. '
|
|
31
31
|
'A bounded summary is not contradictory new evidence. pools_omitted means recorded pool details were left out of this response, not absent from the record. '
|
|
32
32
|
'Use token_read before judging or retracting market details when pools are omitted. Testnet assets are excluded from portfolio valuation; do not infer a zero market price. '
|
|
33
|
+
'Use holdings_query search for token names or symbols, rank for top-N, and aggregate for counts and sums. These scan every approved saved holding on the host and return bounded results. Never page through inventories for ranking, counting or named-token questions. Use one search per named token, optionally chain-filtered. Report same-symbol ambiguity with chain and contract. Rank group_by asset combines the priced subset by identity; group_by position ranks wallet positions. Unknown prices are excluded from ranks and counted separately. '
|
|
33
34
|
'assets_omitted and next_offset describe a page, not missing holdings. Use wallet_read with offset and limit for more holdings, and token_read for detailed evidence. '
|
|
34
35
|
'Compact pages defer exit_quote and curve details. exit_quote_recorded true means a validated historical quote exists in the record even if its output is omitted here. Use token_read before judging execution evidence or claiming that a quote is absent. False means no validated quote in this snapshot, not that no exit route exists. '
|
|
35
36
|
'Only an unfiltered complete inventory needs every holdings page. For that request, follow next_offset for each approved wallet using limit 100 and state which pages remain unread if the budget prevents completion. '
|
|
@@ -81,7 +82,7 @@ def read_receipt(name, arguments, result, delivered=True):
|
|
|
81
82
|
receipt['snapshot_id']=result.get('snapshot_id')
|
|
82
83
|
result=result['wallets'][0]
|
|
83
84
|
if isinstance(result,dict):
|
|
84
|
-
for key in ('offset','next_offset','asset_count','position_count','snapshot_id'):
|
|
85
|
+
for key in ('offset','next_offset','asset_count','position_count','snapshot_id','query_scope'):
|
|
85
86
|
if key in result:receipt[key]=result[key]
|
|
86
87
|
for key in ('assets','positions'):
|
|
87
88
|
if isinstance(result.get(key),list):receipt['page_end']=result.get('offset',0)+len(result[key])
|
|
@@ -89,7 +90,12 @@ def read_receipt(name, arguments, result, delivered=True):
|
|
|
89
90
|
|
|
90
91
|
def read_completion(message, receipts, tools):
|
|
91
92
|
"""Validate declared plans and literal requests against delivered receipts."""
|
|
92
|
-
|
|
93
|
+
ranking=re.search(r'\btop[- ]*(\d+|one|two|three|four|five|ten)\b|상위\s*(\d+|다섯|열)',message,re.I)
|
|
94
|
+
requested_rank=None
|
|
95
|
+
if ranking:
|
|
96
|
+
raw=next(v for v in ranking.groups() if v).lower()
|
|
97
|
+
requested_rank=int(raw) if raw.isdigit() else {'one':1,'two':2,'three':3,'four':4,'five':5,'ten':10,'다섯':5,'열':10}[raw]
|
|
98
|
+
names=set(re.findall(r'\b(?:holdings_query|portfolio_read|wallet_read|snapshot_read|snapshots_compare|token_read|job_read|job_list|snapshots_list)\b',message))
|
|
93
99
|
literal_names=names.copy()
|
|
94
100
|
decoder=json.JSONDecoder();requested=[]
|
|
95
101
|
for match in re.finditer(r'\{',message):
|
|
@@ -103,7 +109,7 @@ def read_completion(message, receipts, tools):
|
|
|
103
109
|
for read in plan['reads']:
|
|
104
110
|
if isinstance(read,dict) and isinstance(read.get('name'),str) and isinstance(read.get('arguments'),dict):
|
|
105
111
|
names.add(read['name']);requested.append((read['name'],read['arguments']))
|
|
106
|
-
if not names or not requested and not re.search(r'\b(?:read|call|run|use|execute)\b|읽|호출|실행|수행|\+',message,re.I):return None
|
|
112
|
+
if not requested_rank and (not names or not requested and not re.search(r'\b(?:read|call|run|use|execute)\b|읽|호출|실행|수행|\+',message,re.I)):return None
|
|
107
113
|
expanded=[]
|
|
108
114
|
for receipt in receipts:
|
|
109
115
|
expanded.extend(receipt.get('reads',[]) if receipt['name']=='records_read' else [receipt])
|
|
@@ -174,7 +180,8 @@ def read_completion(message, receipts, tools):
|
|
|
174
180
|
exhaustive=bool(re.search(r'\b(?:complete|full)\s+inventory\b|\ball\s+(?:holdings|assets|positions)\b|전체\s*(?:보유|자산|목록)',message,re.I))
|
|
175
181
|
groups={}
|
|
176
182
|
for r in expanded:
|
|
177
|
-
if r['name'] not in ('wallet_read','snapshot_read','snapshots_compare','inventory_page'):continue
|
|
183
|
+
if r['name'] not in ('wallet_read','snapshot_read','snapshots_compare','inventory_page','holdings_query'):continue
|
|
184
|
+
if r['name']=='holdings_query' and (r.get('query_scope') or {}).get('mode')!='search':continue
|
|
178
185
|
if not isinstance(r.get('arguments'),dict):continue
|
|
179
186
|
if r['status']=='failed' and any(matches(r['name'],r.get('arguments',{}),ok) for ok in completed):continue
|
|
180
187
|
if r['name']=='inventory_page' and not exhaustive:continue
|
|
@@ -194,6 +201,15 @@ def read_completion(message, receipts, tools):
|
|
|
194
201
|
offset=page['next_offset']
|
|
195
202
|
if not finished and (exhaustive or any(r.get('next_offset') is not None for r in pages)):
|
|
196
203
|
unread.append({'name':name,'arguments':{**json.loads(identity),'offset':offset},'snapshot_id':snapshot})
|
|
204
|
+
if requested_rank:
|
|
205
|
+
count=requested_rank
|
|
206
|
+
ranked=[r for r in completed if r['name']=='holdings_query' and (r.get('query_scope') or {}).get('mode')=='rank']
|
|
207
|
+
if not any(q.get('full_inventory_scanned') and q.get('complete') and q.get('offset')==0 and q.get('requested_limit',0)>=count for r in ranked for q in [r['query_scope']]):
|
|
208
|
+
missing.append('holdings_query rank for the requested top '+str(count))
|
|
209
|
+
for r in completed:
|
|
210
|
+
q=r.get('query_scope')
|
|
211
|
+
if q and not q.get('complete'):missing.append('holdings_query result detail was truncated')
|
|
212
|
+
missing.extend('holdings_query has unread matching records' for r in unread if r['name']=='holdings_query')
|
|
197
213
|
if exhaustive:missing.extend(r['name']+' unread offset '+str(r['arguments']['offset']) for r in unread)
|
|
198
214
|
return {'status':'partial' if missing or failed else 'complete','missing':list(dict.fromkeys(missing)),'failed':failed,'unread_pages':unread,'completed_reads':sum(r['name']!='inventory_page' for r in completed)}
|
|
199
215
|
|
|
@@ -251,8 +267,7 @@ def config_input(value, root):
|
|
|
251
267
|
else: result['wallet'] = None
|
|
252
268
|
return result
|
|
253
269
|
|
|
254
|
-
def
|
|
255
|
-
if config['scope'] == 'none': return {'scope':'none','note':'No automatic wallet context supplied.'}
|
|
270
|
+
def scoped_wallets(root,config):
|
|
256
271
|
import sys
|
|
257
272
|
sys.path.insert(0,str(Path(__file__).resolve().parent/'viewer'))
|
|
258
273
|
from model import load_state
|
|
@@ -261,6 +276,11 @@ def projection(root, config, *, token_id=None, limit=12, offset=0, wallet_key=No
|
|
|
261
276
|
if config['scope'] == 'wallet':
|
|
262
277
|
wallets = [w for w in wallets if w['key'] == config['wallet']]
|
|
263
278
|
if len(wallets) != 1: raise JobError('wallet_missing','The approved wallet is no longer registered. Review your context settings.')
|
|
279
|
+
return wallets
|
|
280
|
+
|
|
281
|
+
def projection(root, config, *, token_id=None, limit=12, offset=0, wallet_key=None, inventory=False):
|
|
282
|
+
if config['scope'] == 'none': return {'scope':'none','note':'No automatic wallet context supplied.'}
|
|
283
|
+
wallets=scoped_wallets(root,config)
|
|
264
284
|
wallets=[{**w,'recorded_valuation':recorded_valuation(w['assets'])} for w in wallets]
|
|
265
285
|
if token_id is not None:
|
|
266
286
|
wallets=[{**wallet,'assets':[a for a in wallet['assets'] if a['id']==token_id]} for wallet in wallets]
|
|
@@ -349,7 +369,7 @@ def wallet_facts(wallets, *, pool_budget=40_000):
|
|
|
349
369
|
item['markets'] = asset.get('links',[])
|
|
350
370
|
pools=asset.get('market_pools',[]);item['pools']=[];item['pools_recorded']=len(pools)
|
|
351
371
|
for pool in pools[:12]:
|
|
352
|
-
fact={**pick(pool,('venue','pool','url','liquidity_usd','price_usd','observed_at')),
|
|
372
|
+
fact={**pick(pool,('venue','pool','url','liquidity_usd','price_usd','observed_at','version_labels','fee_bps','pool_kind')),
|
|
353
373
|
'pair':[pick(coin,('address','symbol')) for coin in pool.get('pair',[])]}
|
|
354
374
|
cost=len(json.dumps(fact,ensure_ascii=False).encode())+2
|
|
355
375
|
if cost>pool_budget:break
|
|
@@ -615,7 +635,7 @@ class AgentStore:
|
|
|
615
635
|
def _run(self,key,config,prompt,message,epoch):
|
|
616
636
|
try:
|
|
617
637
|
turn=self.turns[key];cancel=turn['cancel']
|
|
618
|
-
tools=None;transcript=[];repair=None;repaired=False;completion_retried=False;planning=False;deadline=time.monotonic()+420
|
|
638
|
+
tools=None;transcript=[];blocked_context=set();repair=None;repaired=False;completion_retried=False;planning=False;deadline=time.monotonic()+420
|
|
619
639
|
if not turn['test'] and config.get('wallet_tools') and config['scope']!='none':
|
|
620
640
|
from kira_research_tools import ResearchTools,CATALOG
|
|
621
641
|
@contextmanager
|
|
@@ -658,6 +678,8 @@ class AgentStore:
|
|
|
658
678
|
repair='No tool was executed. First return only a records_read plan with every requested source, or an empty reads list for an action-only request.';repaired=True;continue
|
|
659
679
|
repair=None
|
|
660
680
|
if step==8:raise JobError('tool_limit','The research step limit was reached. Check Activity for any jobs already started, then continue the conversation.')
|
|
681
|
+
signature=json.dumps(request,sort_keys=True,ensure_ascii=False)
|
|
682
|
+
if signature in blocked_context:raise JobError('context_limit','This request keeps returning too much detail. Ask for a token search, a ranking or an aggregate summary. Earlier completed reads are retained.')
|
|
661
683
|
try:
|
|
662
684
|
fields(request,['kira_tool','arguments'])
|
|
663
685
|
with self.lock:
|
|
@@ -666,7 +688,7 @@ class AgentStore:
|
|
|
666
688
|
result=tools.call(request['kira_tool'],request['arguments'],str(uuid.uuid5(uuid.UUID(key),str(step))))
|
|
667
689
|
if request['kira_tool']=='records_read':planning=False
|
|
668
690
|
executed_result=result
|
|
669
|
-
if len(json.dumps(result,ensure_ascii=False).encode())>(280000 if request['kira_tool']=='records_read' else 160000):result={'error':'This result exceeds the context limit.
|
|
691
|
+
if len(json.dumps(result,ensure_ascii=False).encode())>(280000 if request['kira_tool']=='records_read' else 160000):result={'error':{'code':'context_limit','message':'This result exceeds the context limit. Use holdings_query for names, rankings or aggregates, or request a smaller page.'}}
|
|
670
692
|
except JobError as error:
|
|
671
693
|
if error.code=='cancelled':raise
|
|
672
694
|
result={'error':{'code':error.code,'message':str(error)}}
|
|
@@ -674,6 +696,7 @@ class AgentStore:
|
|
|
674
696
|
item={'tool':request['kira_tool'],'arguments':request['arguments'],'result':result}
|
|
675
697
|
if len(json.dumps(transcript+[item],ensure_ascii=False).encode())>280000:
|
|
676
698
|
item['result']={'error':{'code':'context_limit','message':'The accumulated research context limit was reached. Use a smaller wallet_read page or a specific token, or finish with the evidence already received.'}}
|
|
699
|
+
if isinstance(item['result'],dict) and isinstance(item['result'].get('error'),dict) and item['result']['error'].get('code') in ('context_limit','context_too_large'):blocked_context.add(signature)
|
|
677
700
|
transcript.append(item)
|
|
678
701
|
with self.lock:
|
|
679
702
|
delivered=item['result']
|
|
@@ -693,8 +716,7 @@ class AgentStore:
|
|
|
693
716
|
completion=read_completion(message,turn.get('tool_calls',[]),tools)
|
|
694
717
|
turn['read_completion']=completion
|
|
695
718
|
if completion and completion['status']=='partial':
|
|
696
|
-
|
|
697
|
-
answer=('요청한 자료 읽기를 모두 완료하지 못했습니다: ' if re.search(r'[가-힣]',message) else 'Requested reads are incomplete: ')+missing+'.\n\n'+answer
|
|
719
|
+
answer=('저장된 자료 중 요청에 필요한 일부 기록을 확인하지 못했습니다. 아래 답변은 확인된 범위만 포함합니다.' if re.search(r'[가-힣]',message) else 'Some requested records could not be checked. This answer covers only the available evidence.')+'\n\n'+answer
|
|
698
720
|
self.messages.extend([{'role':'user','text':message},{'role':'assistant','text':answer}]);self.messages=self.messages[-24:]
|
|
699
721
|
self.updated_at=datetime.now(timezone.utc).isoformat();self._remember()
|
|
700
722
|
turn.update(state='partial' if (turn.get('read_completion') or {}).get('status')=='partial' else 'succeeded',answer=answer)
|
package/kira_jobs.py
CHANGED
|
@@ -191,8 +191,27 @@ class JobStore:
|
|
|
191
191
|
final=published_asset_progress(path,(stat.st_mtime_ns,stat.st_size,stat.st_ino))
|
|
192
192
|
if final and final[0]==wallet.lower():job['asset_progress']={chain:dict(row) for chain,row in final[1]}
|
|
193
193
|
except (OSError,ValueError,TypeError,AttributeError):pass
|
|
194
|
+
if job['state']=='failed' and any(e.get('code')=='engine_failed' for e in job['errors']):
|
|
195
|
+
failure=self.engine_failure(job)
|
|
196
|
+
job['errors']=[{'code':failure.code,'message':str(failure)}]
|
|
194
197
|
return job
|
|
195
198
|
|
|
199
|
+
def engine_failure(self, job):
|
|
200
|
+
"""Expose only recognized failure categories, never engine diagnostics.
|
|
201
|
+
|
|
202
|
+
Older checkpoints predate error codes. Project their exact known error
|
|
203
|
+
without rewriting the historical job or leaking provider responses.
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
run=read(self.root/job['checkpoint']/'run.json')
|
|
207
|
+
wallet=job['input'].get('wallet') or job['input'].get('address')
|
|
208
|
+
if run.get('status')=='failed' and run.get('wallet_address','').lower()==wallet.lower():
|
|
209
|
+
if (run.get('error_code')=='network_scope_changed' or
|
|
210
|
+
run.get('error')=='Mint Club SDK network/deployment scope changed. Reconcile networks.json before analysis.'):
|
|
211
|
+
return JobError('network_scope_changed',"Kira's network configuration did not pass its compatibility check. Update Kira, then retry.")
|
|
212
|
+
except (OSError,ValueError,TypeError,AttributeError):pass
|
|
213
|
+
return JobError('engine_failed','The lookup stopped unexpectedly. Retry to continue from any saved checks.')
|
|
214
|
+
|
|
196
215
|
def list(self):
|
|
197
216
|
with self.locked():
|
|
198
217
|
return sorted((self.get(p.stem) for p in self.jobs.glob('*.json')), key=lambda j: j['created_at'], reverse=True)
|
|
@@ -702,7 +721,7 @@ class JobStore:
|
|
|
702
721
|
if yielded:raise JobError('yielded','Detailed research yielded to a short operation. Saved checkpoints are retained and detailed research resumes automatically.')
|
|
703
722
|
if self.get(job['job_id'])['stage']=='market_paused':raise JobError('market_budget','Balances are saved. Market verification reached its time budget. Resume this job in Activity to continue saved checks. Pending pools are not verified absent.')
|
|
704
723
|
if self.get(job['job_id'])['stage']=='research_paused':raise JobError('research_pending','Research has saved unfinished ranges ready for continuation.')
|
|
705
|
-
raise
|
|
724
|
+
raise self.engine_failure(job)
|
|
706
725
|
finally:
|
|
707
726
|
stop_child(); selector.close(); child.stdout.close()
|
|
708
727
|
|
package/kira_research_tools.py
CHANGED
|
@@ -9,6 +9,7 @@ CATALOG = [
|
|
|
9
9
|
{'name':'records_read','arguments':{'reads':'List of 0 to 32 declared read tools, each with name and arguments.'},'description':'Declare and execute the complete requested read plan, including every requested source. Includes one receipt per read. An empty plan is for greetings or action-only requests. Read tools only; never queues research.'},
|
|
10
10
|
{'name':'portfolio_read','arguments':{},'description':'Read approved wallet summaries and coverage. Includes a bounded first holdings page; use wallet_read for more.'},
|
|
11
11
|
{'name':'wallet_read','arguments':{'wallet':'registered address or exact name'},'optional_arguments':{'offset':'nonnegative integer, default 0','limit':'integer 1 to 100, default 100'},'description':'Read a compact holdings page of one approved wallet. Follow next_offset until null for the complete recorded inventory. Use token_read for curve, market and exit details.'},
|
|
12
|
+
{'name':'holdings_query','arguments':{'mode':'search, rank or aggregate'},'optional_arguments':{'query':'Token symbol, name, exact contract or native. Required for search. Use one query per token.','wallet':'Optional registered wallet name or address; omit for every approved wallet.','chain_id':'Optional positive chain ID','environment':'mainnet (default), testnet or all','group_by':'asset (default: sum priced positions by chain and contract) or position','minimum_value_usd':'Optional nonnegative USD minimum per position; unknowns excluded and counted.','offset':'nonnegative integer, default 0','limit':'integer 1 to 25, default 10'},'description':'Search names, rank top N or aggregate the ENTIRE approved saved inventory on the host. No inventory pagination or jobs needed. Returns exact counts, values, ambiguity and bounded detailed matches. Use search for symbol-only questions, rank for top-N questions and aggregate for counts or sums.'},
|
|
12
13
|
{'name':'token_read','arguments':{'chain_id':'positive integer','address':'contract address or native'},'optional_arguments':{'minimum_value_usd':'Nonnegative USD number or decimal string. Filter by recorded mainnet value; unknowns stay separate.'},'description':'Read this asset across ALL approved wallets in one request, including saved native observations. Use the optional minimum for value-threshold questions and the exact returned position_count. No wallet inventory pagination is needed.'},
|
|
13
14
|
{'name':'snapshots_list','arguments':{'wallet':'registered address or exact name'},'description':'List past saved analyses for an approved wallet.'},
|
|
14
15
|
{'name':'snapshot_read','arguments':{'snapshot_id':'saved snapshot ID'},'optional_arguments':{'offset':'nonnegative integer, default 0','limit':'integer 1 to 100, default 20'},'description':'Read a compact page of historical evidence for an approved wallet.'},
|
|
@@ -86,7 +87,7 @@ class ResearchTools:
|
|
|
86
87
|
if not isinstance(arguments,dict):raise JobError('invalid_tool','Expected a read plan.')
|
|
87
88
|
fields(arguments,['reads']);reads=arguments['reads']
|
|
88
89
|
if not isinstance(reads,list) or not 0<=len(reads)<=32:raise JobError('invalid_tool','Use at most 32 read requests.')
|
|
89
|
-
allowed={'portfolio_read','wallet_read','token_read','snapshots_list','snapshot_read','snapshots_compare','job_list','job_read'}
|
|
90
|
+
allowed={'holdings_query','portfolio_read','wallet_read','token_read','snapshots_list','snapshot_read','snapshots_compare','job_list','job_read'}
|
|
90
91
|
for read in reads:
|
|
91
92
|
if not isinstance(read,dict):raise JobError('invalid_tool','Expected read requests.')
|
|
92
93
|
fields(read,['name','arguments'])
|
|
@@ -124,6 +125,14 @@ class ResearchTools:
|
|
|
124
125
|
elif not isinstance(value,str) or not value or len(value)>500:
|
|
125
126
|
raise JobError('invalid_tool','Expected a bounded recorded identifier.')
|
|
126
127
|
context,approved=self.allowed()
|
|
128
|
+
if name=='holdings_query':
|
|
129
|
+
from kira_agent import scoped_wallets
|
|
130
|
+
from holdings_query import query_holdings
|
|
131
|
+
wallets=scoped_wallets(self.root,self.config)
|
|
132
|
+
if 'wallet' in arguments:
|
|
133
|
+
selected=self.wallet(arguments['wallet'],approved)
|
|
134
|
+
wallets=[w for w in wallets if w['key']==selected]
|
|
135
|
+
return query_holdings(wallets,arguments)
|
|
127
136
|
if name=='portfolio_read':return context
|
|
128
137
|
if name=='wallet_read':
|
|
129
138
|
wallet=self.wallet(arguments['wallet'],approved)
|